From 45f883b7728bc733b08ce4c01d7f16c4e84dbe7b Mon Sep 17 00:00:00 2001 From: zhengshunjin <2667579369@qq.com> Date: Wed, 10 Jun 2026 04:09:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(project):=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=BB=93=E6=9E=84=E5=92=8C=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/01-bugReport.yml | 65 + .github/ISSUE_TEMPLATE/02-featureRequest.yml | 40 + .github/ISSUE_TEMPLATE/config.yml | 11 + .github/dependabot.yml | 45 + .github/scripts/cronet.sh | 92 + .github/scripts/lzy_web.py | 98 + .github/scripts/tg_bot.py | 47 + .github/workflows/autoupdatefork.yml | 26 + .github/workflows/cronet.yml | 53 + .github/workflows/legado.jks | Bin 0 -> 2232 bytes .github/workflows/release.yml | 140 + .github/workflows/stale.yml | 29 + .github/workflows/test.yml | 278 ++ .github/workflows/web.yml | 65 + .gitignore | 18 + CHANGELOG.md | 11 + English.md | 149 + LICENSE | 674 +++ api.md | 293 ++ app/.gitignore | 2 + app/build.gradle | 292 ++ app/cronet-proguard-rules.pro | 232 + app/cronetlib/cronet_api.jar | Bin 0 -> 176441 bytes app/cronetlib/cronet_impl_common_java.jar | Bin 0 -> 112934 bytes app/cronetlib/cronet_impl_native_java.jar | Bin 0 -> 1419896 bytes app/cronetlib/cronet_impl_platform_java.jar | Bin 0 -> 72250 bytes app/cronetlib/cronet_shared_java.jar | Bin 0 -> 78508 bytes app/download.gradle | 112 + app/google-services.json | 151 + app/proguard-rules.pro | 142 + .../io.legado.app.data.AppDatabase/1.json | 1022 +++++ .../io.legado.app.data.AppDatabase/10.json | 1176 +++++ .../io.legado.app.data.AppDatabase/11.json | 1182 +++++ .../io.legado.app.data.AppDatabase/12.json | 1188 +++++ .../io.legado.app.data.AppDatabase/13.json | 1194 +++++ .../io.legado.app.data.AppDatabase/14.json | 1194 +++++ .../io.legado.app.data.AppDatabase/15.json | 1200 +++++ .../io.legado.app.data.AppDatabase/16.json | 1226 +++++ .../io.legado.app.data.AppDatabase/17.json | 1226 +++++ .../io.legado.app.data.AppDatabase/18.json | 1258 ++++++ .../io.legado.app.data.AppDatabase/19.json | 1265 ++++++ .../io.legado.app.data.AppDatabase/2.json | 1028 +++++ .../io.legado.app.data.AppDatabase/20.json | 1271 ++++++ .../io.legado.app.data.AppDatabase/21.json | 1283 ++++++ .../io.legado.app.data.AppDatabase/22.json | 1277 ++++++ .../io.legado.app.data.AppDatabase/23.json | 1283 ++++++ .../io.legado.app.data.AppDatabase/24.json | 1324 ++++++ .../io.legado.app.data.AppDatabase/25.json | 1368 ++++++ .../io.legado.app.data.AppDatabase/26.json | 1392 ++++++ .../io.legado.app.data.AppDatabase/27.json | 1392 ++++++ .../io.legado.app.data.AppDatabase/28.json | 1404 ++++++ .../io.legado.app.data.AppDatabase/29.json | 1410 ++++++ .../io.legado.app.data.AppDatabase/3.json | 1036 +++++ .../io.legado.app.data.AppDatabase/30.json | 1485 +++++++ .../io.legado.app.data.AppDatabase/31.json | 1422 ++++++ .../io.legado.app.data.AppDatabase/32.json | 1422 ++++++ .../io.legado.app.data.AppDatabase/33.json | 1417 ++++++ .../io.legado.app.data.AppDatabase/34.json | 1423 ++++++ .../io.legado.app.data.AppDatabase/35.json | 1441 ++++++ .../io.legado.app.data.AppDatabase/36.json | 1441 ++++++ .../io.legado.app.data.AppDatabase/37.json | 1459 ++++++ .../io.legado.app.data.AppDatabase/38.json | 1465 ++++++ .../io.legado.app.data.AppDatabase/39.json | 1471 ++++++ .../io.legado.app.data.AppDatabase/4.json | 1093 +++++ .../io.legado.app.data.AppDatabase/40.json | 1483 ++++++ .../io.legado.app.data.AppDatabase/41.json | 1513 +++++++ .../io.legado.app.data.AppDatabase/42.json | 1519 +++++++ .../io.legado.app.data.AppDatabase/43.json | 1537 +++++++ .../io.legado.app.data.AppDatabase/44.json | 1557 +++++++ .../io.legado.app.data.AppDatabase/45.json | 1618 +++++++ .../io.legado.app.data.AppDatabase/46.json | 1618 +++++++ .../io.legado.app.data.AppDatabase/47.json | 1625 +++++++ .../io.legado.app.data.AppDatabase/48.json | 1633 +++++++ .../io.legado.app.data.AppDatabase/49.json | 1654 +++++++ .../io.legado.app.data.AppDatabase/5.json | 1093 +++++ .../io.legado.app.data.AppDatabase/50.json | 1666 +++++++ .../io.legado.app.data.AppDatabase/51.json | 1674 +++++++ .../io.legado.app.data.AppDatabase/52.json | 1680 +++++++ .../io.legado.app.data.AppDatabase/53.json | 1692 +++++++ .../io.legado.app.data.AppDatabase/54.json | 1711 +++++++ .../io.legado.app.data.AppDatabase/55.json | 1711 +++++++ .../io.legado.app.data.AppDatabase/56.json | 1711 +++++++ .../io.legado.app.data.AppDatabase/57.json | 1717 +++++++ .../io.legado.app.data.AppDatabase/58.json | 1724 +++++++ .../io.legado.app.data.AppDatabase/59.json | 1740 ++++++++ .../io.legado.app.data.AppDatabase/6.json | 1131 +++++ .../io.legado.app.data.AppDatabase/60.json | 1778 ++++++++ .../io.legado.app.data.AppDatabase/61.json | 1786 ++++++++ .../io.legado.app.data.AppDatabase/62.json | 1830 ++++++++ .../io.legado.app.data.AppDatabase/63.json | 1838 ++++++++ .../io.legado.app.data.AppDatabase/64.json | 1847 ++++++++ .../io.legado.app.data.AppDatabase/65.json | 1853 ++++++++ .../io.legado.app.data.AppDatabase/66.json | 1859 ++++++++ .../io.legado.app.data.AppDatabase/67.json | 1879 ++++++++ .../io.legado.app.data.AppDatabase/68.json | 1885 ++++++++ .../io.legado.app.data.AppDatabase/69.json | 1885 ++++++++ .../io.legado.app.data.AppDatabase/7.json | 1131 +++++ .../io.legado.app.data.AppDatabase/70.json | 1891 ++++++++ .../io.legado.app.data.AppDatabase/71.json | 1898 ++++++++ .../io.legado.app.data.AppDatabase/72.json | 1912 ++++++++ .../io.legado.app.data.AppDatabase/73.json | 1918 ++++++++ .../io.legado.app.data.AppDatabase/74.json | 1930 ++++++++ .../io.legado.app.data.AppDatabase/75.json | 1935 ++++++++ .../io.legado.app.data.AppDatabase/8.json | 1157 +++++ .../io.legado.app.data.AppDatabase/9.json | 1164 +++++ .../java/io/legado/app/AndroidJsTest.kt | 84 + .../io/legado/app/ExampleInstrumentedTest.kt | 34 + .../java/io/legado/app/HttpTest.kt | 83 + .../java/io/legado/app/HttpTtsTest.kt | 26 + .../java/io/legado/app/MigrationTest.kt | 51 + .../java/io/legado/app/UpdateTest.kt | 54 + app/src/debug/res/values-zh/strings.xml | 4 + app/src/debug/res/values/strings.xml | 4 + app/src/main/AndroidManifest.xml | 549 +++ app/src/main/assets/18PlusList.txt | 284 ++ app/src/main/assets/LICENSE.md | 674 +++ app/src/main/assets/bg/午后沙滩.jpg | Bin 0 -> 8100 bytes app/src/main/assets/bg/宁静夜色.jpg | Bin 0 -> 20037 bytes app/src/main/assets/bg/山水墨影.jpg | Bin 0 -> 6462 bytes app/src/main/assets/bg/山水画.jpg | Bin 0 -> 14244 bytes app/src/main/assets/bg/护眼漫绿.jpg | Bin 0 -> 92965 bytes app/src/main/assets/bg/新羊皮纸.jpg | Bin 0 -> 26470 bytes app/src/main/assets/bg/明媚倾城.jpg | Bin 0 -> 10362 bytes app/src/main/assets/bg/深宫魅影.jpg | Bin 0 -> 8452 bytes app/src/main/assets/bg/清新时光.jpg | Bin 0 -> 8462 bytes app/src/main/assets/bg/羊皮纸1.jpg | Bin 0 -> 202199 bytes app/src/main/assets/bg/羊皮纸2.jpg | Bin 0 -> 319831 bytes app/src/main/assets/bg/羊皮纸3.jpg | Bin 0 -> 123242 bytes app/src/main/assets/bg/羊皮纸4.jpg | Bin 0 -> 110907 bytes app/src/main/assets/bg/边彩画布.jpg | Bin 0 -> 76772 bytes app/src/main/assets/cronet.json | 1 + .../main/assets/defaultData/bookSources.json | 48 + .../main/assets/defaultData/coverRule.json | 5 + .../main/assets/defaultData/dictRules.json | 23 + .../assets/defaultData/directLinkUpload.json | 20 + app/src/main/assets/defaultData/httpTTS.json | 30 + .../assets/defaultData/keyboardAssists.json | 162 + .../main/assets/defaultData/readConfig.json | 105 + .../main/assets/defaultData/rssSources.json | 42 + .../main/assets/defaultData/themeConfig.json | 34 + .../main/assets/defaultData/txtTocRule.json | 210 + app/src/main/assets/disclaimer.md | 16 + app/src/main/assets/epub/chapter.html | 13 + app/src/main/assets/epub/cover.html | 21 + app/src/main/assets/epub/fonts.css | 267 ++ app/src/main/assets/epub/intro.html | 11 + app/src/main/assets/epub/logo.png | Bin 0 -> 24175 bytes app/src/main/assets/epub/main.css | 551 +++ app/src/main/assets/font/number.ttf | Bin 0 -> 6000 bytes app/src/main/assets/privacyPolicy.md | 5 + app/src/main/assets/storageHelp.md | 2 + app/src/main/assets/updateLog.md | 41 + app/src/main/assets/web/assets/css/main.css | 3956 +++++++++++++++++ app/src/main/assets/web/assets/js/dist.js | 8 + app/src/main/assets/web/assets/js/md5.js | 256 ++ app/src/main/assets/web/favicon.ico | Bin 0 -> 4286 bytes .../help/css/github-markdown-light.min.css | 1 + .../assets/web/help/css/highlight.min.css | 132 + app/src/main/assets/web/help/index.html | 21 + .../main/assets/web/help/js/highlight.min.js | 1213 +++++ app/src/main/assets/web/help/js/main.js | 41 + .../web/help/js/marked-highlight.umd.js | 3 + app/src/main/assets/web/help/js/marked.min.js | 6 + app/src/main/assets/web/help/js/require.js | 5 + .../web/help/md/ExtensionContentType.md | 156 + .../assets/web/help/md/SourceMBookHelp.md | 29 + .../main/assets/web/help/md/SourceMRssHelp.md | 20 + app/src/main/assets/web/help/md/appHelp.md | 222 + app/src/main/assets/web/help/md/debugHelp.md | 22 + .../main/assets/web/help/md/dictRuleHelp.md | 7 + .../main/assets/web/help/md/httpTTSHelp.md | 15 + app/src/main/assets/web/help/md/jsHelp.md | 461 ++ .../main/assets/web/help/md/readMenuHelp.md | 46 + app/src/main/assets/web/help/md/regexHelp.md | 204 + .../assets/web/help/md/replaceRuleHelp.md | 6 + app/src/main/assets/web/help/md/ruleHelp.md | 246 + .../main/assets/web/help/md/txtTocRuleHelp.md | 16 + .../main/assets/web/help/md/webDavBookHelp.md | 60 + app/src/main/assets/web/help/md/webDavHelp.md | 19 + app/src/main/assets/web/help/md/xpathHelp.md | 265 ++ app/src/main/assets/web/images/bg.jpg | Bin 0 -> 50755 bytes app/src/main/assets/web/index.html | 72 + .../assets/web/uploadBook/css/wifi_send.css | 356 ++ .../main/assets/web/uploadBook/img/close.png | Bin 0 -> 1018 bytes .../main/assets/web/uploadBook/img/logo.png | Bin 0 -> 2621 bytes .../assets/web/uploadBook/img/notice01.png | Bin 0 -> 4348 bytes .../assets/web/uploadBook/img/notice02.png | Bin 0 -> 9496 bytes .../main/assets/web/uploadBook/img/right.png | Bin 0 -> 1124 bytes .../main/assets/web/uploadBook/img/safari.png | Bin 0 -> 10146 bytes .../main/assets/web/uploadBook/img/wrong.png | Bin 0 -> 1345 bytes app/src/main/assets/web/uploadBook/index.html | 357 ++ .../main/assets/web/uploadBook/js/common.js | 173 + .../assets/web/uploadBook/js/html5_fun.js | 257 ++ .../web/uploadBook/js/jquery-1.4.2.min.js | 154 + .../web/vue/assets/BookChapter-BsiFtdIw.css | 1 + .../web/vue/assets/BookChapter-Cs3stH93.js | 1 + .../web/vue/assets/BookShelf-00b2QCsd.css | 1 + .../web/vue/assets/BookShelf-DIQtBULC.js | 1 + .../web/vue/assets/iconfont-PstzbNMW.woff | Bin 0 -> 30972 bytes .../assets/web/vue/assets/index-CrxHVQK7.css | 1 + .../assets/web/vue/assets/index-Wr40-hHf.js | 10 + .../assets/web/vue/assets/loading-C4J6hIxs.js | 1 + .../web/vue/assets/loading-DkQYEuap.css | 1 + .../web/vue/assets/popfont-WaOB0hHG.ttf | Bin 0 -> 4184 bytes .../web/vue/assets/shelffont-D-W4UqG-.ttf | Bin 0 -> 10072 bytes .../assets/web/vue/assets/vendor-CXe1BRiH.css | 1 + .../assets/web/vue/assets/vendor-KSDcS24u.js | 54 + app/src/main/assets/web/vue/favicon.ico | Bin 0 -> 4286 bytes app/src/main/assets/web/vue/index.html | 16 + app/src/main/ic_launcher-web.png | Bin 0 -> 204579 bytes app/src/main/java/io/legado/app/App.kt | 262 ++ app/src/main/java/io/legado/app/README.md | 15 + .../java/io/legado/app/api/ReaderProvider.kt | 164 + .../main/java/io/legado/app/api/ReturnData.kt | 29 + .../main/java/io/legado/app/api/ShortCuts.kt | 63 + .../app/api/controller/BookController.kt | 325 ++ .../api/controller/BookSourceController.kt | 80 + .../api/controller/ReplaceRuleController.kt | 97 + .../app/api/controller/RssSourceController.kt | 77 + .../io/legado/app/base/AppContextWrapper.kt | 99 + .../java/io/legado/app/base/BaseActivity.kt | 216 + .../io/legado/app/base/BaseDialogFragment.kt | 114 + .../java/io/legado/app/base/BaseFragment.kt | 78 + .../legado/app/base/BasePrefDialogFragment.kt | 44 + .../java/io/legado/app/base/BaseService.kt | 107 + .../java/io/legado/app/base/BaseViewModel.kt | 52 + .../main/java/io/legado/app/base/README.md | 1 + .../java/io/legado/app/base/VMBaseActivity.kt | 17 + .../java/io/legado/app/base/VMBaseFragment.kt | 9 + .../app/base/adapter/DiffRecyclerAdapter.kt | 223 + .../legado/app/base/adapter/ItemAnimation.kt | 85 + .../legado/app/base/adapter/ItemViewHolder.kt | 10 + .../app/base/adapter/RecyclerAdapter.kt | 486 ++ .../adapter/animations/AlphaInAnimation.kt | 18 + .../base/adapter/animations/BaseAnimation.kt | 13 + .../adapter/animations/ScaleInAnimation.kt | 21 + .../animations/SlideInBottomAnimation.kt | 12 + .../animations/SlideInLeftAnimation.kt | 13 + .../animations/SlideInRightAnimation.kt | 13 + .../java/io/legado/app/constant/AppConst.kt | 111 + .../java/io/legado/app/constant/AppLog.kt | 64 + .../java/io/legado/app/constant/AppPattern.kt | 58 + .../io/legado/app/constant/BookSourceType.kt | 18 + .../java/io/legado/app/constant/BookType.kt | 73 + .../java/io/legado/app/constant/EventBus.kt | 37 + .../io/legado/app/constant/IntentAction.kt | 25 + .../io/legado/app/constant/NotificationId.kt | 19 + .../java/io/legado/app/constant/PageAnim.kt | 23 + .../java/io/legado/app/constant/PreferKey.kt | 189 + .../java/io/legado/app/constant/SourceType.kt | 16 + .../java/io/legado/app/constant/Status.kt | 7 + .../main/java/io/legado/app/constant/Theme.kt | 5 + .../java/io/legado/app/data/AppDatabase.kt | 244 + .../io/legado/app/data/DatabaseMigrations.kt | 374 ++ .../main/java/io/legado/app/data/README.md | 18 + .../io/legado/app/data/dao/BookChapterDao.kt | 46 + .../java/io/legado/app/data/dao/BookDao.kt | 171 + .../io/legado/app/data/dao/BookGroupDao.kt | 103 + .../io/legado/app/data/dao/BookSourceDao.kt | 372 ++ .../io/legado/app/data/dao/BookmarkDao.kt | 60 + .../java/io/legado/app/data/dao/CacheDao.kt | 35 + .../java/io/legado/app/data/dao/CookieDao.kt | 26 + .../io/legado/app/data/dao/DictRuleDao.kt | 32 + .../java/io/legado/app/data/dao/HttpTTSDao.kt | 36 + .../legado/app/data/dao/KeyboardAssistsDao.kt | 34 + .../io/legado/app/data/dao/ReadRecordDao.kt | 55 + .../io/legado/app/data/dao/ReplaceRuleDao.kt | 114 + .../io/legado/app/data/dao/RssArticleDao.kt | 40 + .../legado/app/data/dao/RssReadRecordDao.kt | 21 + .../io/legado/app/data/dao/RssSourceDao.kt | 157 + .../java/io/legado/app/data/dao/RssStarDao.kt | 45 + .../java/io/legado/app/data/dao/RuleSubDao.kt | 30 + .../io/legado/app/data/dao/SearchBookDao.kt | 73 + .../legado/app/data/dao/SearchKeywordDao.kt | 38 + .../java/io/legado/app/data/dao/ServerDao.kt | 33 + .../io/legado/app/data/dao/TxtTocRuleDao.kt | 45 + .../io/legado/app/data/entities/BaseBook.kt | 53 + .../app/data/entities/BaseRssArticle.kt | 29 + .../io/legado/app/data/entities/BaseSource.kt | 259 ++ .../java/io/legado/app/data/entities/Book.kt | 416 ++ .../legado/app/data/entities/BookChapter.kt | 176 + .../app/data/entities/BookChapterReview.kt | 15 + .../io/legado/app/data/entities/BookGroup.kt | 75 + .../legado/app/data/entities/BookProgress.kt | 21 + .../io/legado/app/data/entities/BookSource.kt | 310 ++ .../app/data/entities/BookSourcePart.kt | 81 + .../io/legado/app/data/entities/Bookmark.kt | 24 + .../java/io/legado/app/data/entities/Cache.kt | 13 + .../io/legado/app/data/entities/Cookie.kt | 12 + .../io/legado/app/data/entities/DictRule.kt | 50 + .../io/legado/app/data/entities/HttpTTS.kt | 84 + .../app/data/entities/KeyboardAssist.kt | 20 + .../io/legado/app/data/entities/ReadRecord.kt | 14 + .../app/data/entities/ReadRecordShow.kt | 7 + .../legado/app/data/entities/ReplaceRule.kt | 121 + .../io/legado/app/data/entities/RssArticle.kt | 57 + .../legado/app/data/entities/RssReadRecord.kt | 13 + .../io/legado/app/data/entities/RssSource.kt | 182 + .../io/legado/app/data/entities/RssStar.kt | 49 + .../io/legado/app/data/entities/RuleSub.kt | 16 + .../io/legado/app/data/entities/SearchBook.kt | 139 + .../legado/app/data/entities/SearchKeyword.kt | 20 + .../io/legado/app/data/entities/Server.kt | 57 + .../io/legado/app/data/entities/TxtTocRule.kt | 29 + .../app/data/entities/rule/BookInfoRule.kt | 42 + .../app/data/entities/rule/BookListRule.kt | 17 + .../app/data/entities/rule/ContentRule.kt | 41 + .../app/data/entities/rule/ExploreKind.kt | 16 + .../app/data/entities/rule/ExploreRule.kt | 40 + .../app/data/entities/rule/FlexChildStyle.kt | 39 + .../app/data/entities/rule/ReviewRule.kt | 36 + .../io/legado/app/data/entities/rule/RowUi.kt | 23 + .../app/data/entities/rule/SearchRule.kt | 39 + .../legado/app/data/entities/rule/TocRule.kt | 34 + .../app/exception/ConcurrentException.kt | 8 + .../app/exception/ContentEmptyException.kt | 6 + .../app/exception/EmptyFileException.kt | 6 + .../app/exception/InvalidBooksDirException.kt | 3 + .../app/exception/NoBooksDirException.kt | 6 + .../app/exception/NoStackTraceException.kt | 17 + .../app/exception/RegexTimeoutException.kt | 3 + .../legado/app/exception/TocEmptyException.kt | 6 + .../io/legado/app/help/AppFreezeMonitor.kt | 80 + .../main/java/io/legado/app/help/AppWebDav.kt | 338 ++ .../java/io/legado/app/help/CacheManager.kt | 123 + .../legado/app/help/ConcurrentRateLimiter.kt | 135 + .../java/io/legado/app/help/CrashHandler.kt | 190 + .../java/io/legado/app/help/DefaultData.kt | 132 + .../io/legado/app/help/DirectLinkUpload.kt | 116 + .../io/legado/app/help/DispatchersMonitor.kt | 59 + .../java/io/legado/app/help/EventMessage.kt | 49 + .../io/legado/app/help/ExecutorService.kt | 6 + .../java/io/legado/app/help/IntentData.kt | 32 + .../java/io/legado/app/help/IntentHelp.kt | 50 + .../java/io/legado/app/help/JsEncodeUtils.kt | 497 +++ .../java/io/legado/app/help/JsExtensions.kt | 1003 +++++ .../io/legado/app/help/LauncherIconHelp.kt | 65 + .../java/io/legado/app/help/LayoutManager.kt | 73 + .../java/io/legado/app/help/LifecycleHelp.kt | 121 + .../main/java/io/legado/app/help/MediaHelp.kt | 70 + .../main/java/io/legado/app/help/PaintPool.kt | 17 + .../main/java/io/legado/app/help/README.md | 1 + .../io/legado/app/help/ReplaceAnalyzer.kt | 47 + .../io/legado/app/help/RuleBigDataHelp.kt | 146 + .../java/io/legado/app/help/RuleComplete.kt | 88 + app/src/main/java/io/legado/app/help/TTS.kt | 145 + .../io/legado/app/help/book/BookContent.kt | 16 + .../io/legado/app/help/book/BookExtensions.kt | 382 ++ .../java/io/legado/app/help/book/BookHelp.kt | 611 +++ .../io/legado/app/help/book/ContentHelp.kt | 630 +++ .../legado/app/help/book/ContentProcessor.kt | 206 + .../io/legado/app/help/config/AppConfig.kt | 742 ++++ .../io/legado/app/help/config/LocalConfig.kt | 126 + .../legado/app/help/config/ReadBookConfig.kt | 756 ++++ .../legado/app/help/config/ReadTipConfig.kt | 107 + .../io/legado/app/help/config/SourceConfig.kt | 45 + .../io/legado/app/help/config/ThemeConfig.kt | 341 ++ .../help/coroutine/ActivelyCancelException.kt | 12 + .../app/help/coroutine/CompositeCoroutine.kt | 84 + .../io/legado/app/help/coroutine/Coroutine.kt | 252 ++ .../app/help/coroutine/CoroutineContainer.kt | 15 + .../app/help/crypto/AsymmetricCrypto.kt | 86 + .../java/io/legado/app/help/crypto/README.md | 16 + .../java/io/legado/app/help/crypto/Sign.kt | 26 + .../app/help/crypto/SymmetricCryptoAndroid.kt | 47 + .../app/help/exoplayer/ExoPlayerHelper.kt | 143 + .../help/exoplayer/InputStreamDataSource.kt | 81 + .../app/help/glide/AsyncRecycleBitmapPool.kt | 25 + .../app/help/glide/BlurTransformation.kt | 30 + .../legado/app/help/glide/FilePathLoader.kt | 57 + .../io/legado/app/help/glide/GlideHeaders.kt | 11 + .../io/legado/app/help/glide/ImageLoader.kt | 108 + .../app/help/glide/LegadoDataUrlLoader.kt | 92 + .../app/help/glide/LegadoGlideModule.kt | 52 + .../app/help/glide/OkHttpModeLoaderFactory.kt | 21 + .../app/help/glide/OkHttpModelLoader.kt | 28 + .../app/help/glide/OkHttpStreamFetcher.kt | 164 + .../help/glide/progress/OnProgressListener.kt | 3 + .../help/glide/progress/ProgressManager.kt | 62 + .../glide/progress/ProgressResponseBody.kt | 54 + .../legado/app/help/http/BackstageWebView.kt | 334 ++ .../io/legado/app/help/http/CookieManager.kt | 170 + .../io/legado/app/help/http/CookieStore.kt | 123 + .../java/io/legado/app/help/http/Cronet.kt | 29 + .../app/help/http/DecompressInterceptor.kt | 45 + .../io/legado/app/help/http/HttpHelper.kt | 184 + .../app/help/http/ObsoleteUrlFactory.kt | 1201 +++++ .../help/http/OkHttpExceptionInterceptor.kt | 20 + .../io/legado/app/help/http/OkHttpUtils.kt | 198 + .../http/OkhttpUncaughtExceptionHandler.kt | 11 + .../io/legado/app/help/http/RequestMethod.kt | 5 + .../java/io/legado/app/help/http/SSLHelper.kt | 194 + .../io/legado/app/help/http/StrResponse.kt | 84 + .../help/http/api/CookieManagerInterface.kt | 28 + .../legado/app/help/rhino/NativeBaseSource.kt | 46 + .../app/help/source/BaseSourceExtensions.kt | 21 + .../app/help/source/BookSourceExtensions.kt | 121 + .../app/help/source/RssSourceExtensions.kt | 62 + .../io/legado/app/help/source/SourceHelp.kt | 178 + .../app/help/source/SourceVerificationHelp.kt | 120 + .../java/io/legado/app/help/storage/Backup.kt | 297 ++ .../io/legado/app/help/storage/BackupAES.kt | 9 + .../legado/app/help/storage/BackupConfig.kt | 145 + .../legado/app/help/storage/ImportOldData.kt | 373 ++ .../io/legado/app/help/storage/Restore.kt | 315 ++ .../legado/app/help/update/AppReleaseInfo.kt | 76 + .../io/legado/app/help/update/AppUpdate.kt | 25 + .../legado/app/help/update/AppUpdateGitHub.kt | 69 + app/src/main/java/io/legado/app/lib/README.md | 7 + .../java/io/legado/app/lib/aliyun/ALiYun.kt | 10 + .../io/legado/app/lib/cronet/AbsCallBack.kt | 507 +++ .../app/lib/cronet/BodyUploadProvider.kt | 67 + .../legado/app/lib/cronet/CallbackResult.kt | 12 + .../io/legado/app/lib/cronet/CallbackStep.kt | 8 + .../lib/cronet/CronetCoroutineInterceptor.kt | 121 + .../io/legado/app/lib/cronet/CronetHelper.kt | 121 + .../app/lib/cronet/CronetInterceptor.kt | 97 + .../io/legado/app/lib/cronet/CronetLoader.kt | 375 ++ .../app/lib/cronet/LargeBodyUploadProvider.kt | 75 + .../io/legado/app/lib/cronet/NewCallBack.kt | 53 + .../io/legado/app/lib/cronet/OldCallback.kt | 59 + .../io/legado/app/lib/dialogs/AlertBuilder.kt | 106 + .../app/lib/dialogs/AndroidAlertBuilder.kt | 171 + .../legado/app/lib/dialogs/AndroidDialogs.kt | 186 + .../app/lib/dialogs/AndroidSelectors.kt | 90 + .../io/legado/app/lib/dialogs/SelectItem.kt | 13 + .../legado/app/lib/icu4j/CharsetDetector.java | 587 +++ .../io/legado/app/lib/icu4j/CharsetMatch.java | 241 + .../app/lib/icu4j/CharsetRecog_2022.java | 170 + .../app/lib/icu4j/CharsetRecog_UTF8.java | 99 + .../app/lib/icu4j/CharsetRecog_Unicode.java | 198 + .../app/lib/icu4j/CharsetRecog_mbcs.java | 552 +++ .../app/lib/icu4j/CharsetRecog_sbcs.java | 1188 +++++ .../app/lib/icu4j/CharsetRecognizer.java | 53 + .../java/io/legado/app/lib/mobi/KF6Book.kt | 140 + .../java/io/legado/app/lib/mobi/KF8Book.kt | 272 ++ .../java/io/legado/app/lib/mobi/MobiBook.kt | 446 ++ .../java/io/legado/app/lib/mobi/MobiReader.kt | 232 + .../java/io/legado/app/lib/mobi/PDBFile.kt | 49 + .../app/lib/mobi/decompress/CDICData.kt | 6 + .../app/lib/mobi/decompress/Decompressor.kt | 7 + .../mobi/decompress/HuffcdicDecompressor.kt | 133 + .../lib/mobi/decompress/Lz77Decompressor.kt | 49 + .../lib/mobi/decompress/PlainDecompressor.kt | 9 + .../app/lib/mobi/entities/ExthRecordType.kt | 7 + .../app/lib/mobi/entities/FdstHeader.kt | 6 + .../legado/app/lib/mobi/entities/Fragment.kt | 9 + .../legado/app/lib/mobi/entities/IndexData.kt | 8 + .../app/lib/mobi/entities/IndexEntry.kt | 9 + .../legado/app/lib/mobi/entities/IndexTag.kt | 6 + .../app/lib/mobi/entities/IndxHeader.kt | 16 + .../app/lib/mobi/entities/KF6Section.kt | 10 + .../legado/app/lib/mobi/entities/KF8Header.kt | 9 + .../io/legado/app/lib/mobi/entities/KF8Pos.kt | 6 + .../app/lib/mobi/entities/KF8Resource.kt | 7 + .../app/lib/mobi/entities/KF8Section.kt | 14 + .../app/lib/mobi/entities/MobiEntryHeaders.kt | 8 + .../app/lib/mobi/entities/MobiHeader.kt | 22 + .../app/lib/mobi/entities/MobiMetadata.kt | 13 + .../io/legado/app/lib/mobi/entities/NCX.kt | 14 + .../app/lib/mobi/entities/PalmDocHeader.kt | 8 + .../io/legado/app/lib/mobi/entities/Ptagx.kt | 8 + .../legado/app/lib/mobi/entities/Skeleton.kt | 9 + .../io/legado/app/lib/mobi/entities/TOC.kt | 7 + .../app/lib/mobi/entities/TagxHeader.kt | 7 + .../legado/app/lib/mobi/entities/TagxTag.kt | 8 + .../app/lib/mobi/utils/BitwiseExtensions.kt | 7 + .../lib/mobi/utils/ByteBufferExtensions.kt | 72 + .../app/lib/permission/OnErrorCallback.kt | 7 + .../permission/OnPermissionsDeniedCallback.kt | 7 + .../OnPermissionsGrantedCallback.kt | 7 + .../permission/OnPermissionsResultCallback.kt | 11 + .../OnRequestPermissionsResultCallback.kt | 10 + .../app/lib/permission/PermissionActivity.kt | 227 + .../legado/app/lib/permission/Permissions.kt | 90 + .../app/lib/permission/PermissionsCompat.kt | 72 + .../io/legado/app/lib/permission/Request.kt | 228 + .../app/lib/permission/RequestManager.kt | 73 + .../app/lib/permission/RequestPlugins.kt | 20 + .../legado/app/lib/prefs/ColorPreference.kt | 444 ++ .../app/lib/prefs/EditTextPreference.kt | 36 + .../app/lib/prefs/EditTextPreferenceDialog.kt | 66 + .../app/lib/prefs/IconListPreference.kt | 215 + .../app/lib/prefs/ListPreferenceDialog.kt | 71 + .../prefs/MultiSelectListPreferenceDialog.kt | 73 + .../app/lib/prefs/NameListPreference.kt | 42 + .../io/legado/app/lib/prefs/Preference.kt | 132 + .../app/lib/prefs/PreferenceCategory.kt | 58 + .../legado/app/lib/prefs/SwitchPreference.kt | 47 + .../lib/prefs/fragment/PreferenceFragment.kt | 75 + .../app/lib/theme/MaterialValueHelper.kt | 148 + .../java/io/legado/app/lib/theme/Selector.kt | 450 ++ .../io/legado/app/lib/theme/ThemeStore.kt | 342 ++ .../app/lib/theme/ThemeStoreInterface.kt | 90 + .../app/lib/theme/ThemeStorePrefKeys.kt | 30 + .../io/legado/app/lib/theme/ThemeUtils.kt | 44 + .../io/legado/app/lib/theme/TintHelper.kt | 487 ++ .../java/io/legado/app/lib/theme/ViewUtils.kt | 43 + .../theme/view/ThemeBottomNavigationVIew.kt | 52 + .../app/lib/theme/view/ThemeCheckBox.kt | 37 + .../app/lib/theme/view/ThemeEditText.kt | 23 + .../app/lib/theme/view/ThemeProgressBar.kt | 16 + .../app/lib/theme/view/ThemeRadioButton.kt | 38 + .../app/lib/theme/view/ThemeRadioNoButton.kt | 83 + .../legado/app/lib/theme/view/ThemeSeekBar.kt | 19 + .../legado/app/lib/theme/view/ThemeSwitch.kt | 40 + .../io/legado/app/lib/webdav/Authorization.kt | 32 + .../java/io/legado/app/lib/webdav/WebDav.kt | 457 ++ .../legado/app/lib/webdav/WebDavException.kt | 11 + .../io/legado/app/lib/webdav/WebDavFile.kt | 29 + .../java/io/legado/app/model/AudioPlay.kt | 433 ++ .../java/io/legado/app/model/BookCover.kt | 269 ++ .../java/io/legado/app/model/CacheBook.kt | 460 ++ .../java/io/legado/app/model/CheckSource.kt | 69 + .../main/java/io/legado/app/model/Debug.kt | 310 ++ .../main/java/io/legado/app/model/Download.kt | 19 + .../java/io/legado/app/model/ImageProvider.kt | 212 + .../main/java/io/legado/app/model/README.md | 7 + .../java/io/legado/app/model/ReadAloud.kt | 137 + .../main/java/io/legado/app/model/ReadBook.kt | 1054 +++++ .../java/io/legado/app/model/ReadManga.kt | 640 +++ .../java/io/legado/app/model/SharedJsScope.kt | 87 + .../model/analyzeRule/AnalyzeByJSonPath.kt | 172 + .../app/model/analyzeRule/AnalyzeByJSoup.kt | 524 +++ .../app/model/analyzeRule/AnalyzeByRegex.kt | 60 + .../app/model/analyzeRule/AnalyzeByXPath.kt | 155 + .../app/model/analyzeRule/AnalyzeRule.kt | 907 ++++ .../app/model/analyzeRule/AnalyzeUrl.kt | 851 ++++ .../legado/app/model/analyzeRule/CustomUrl.kt | 49 + .../app/model/analyzeRule/QueryTTF.java | 1055 +++++ .../app/model/analyzeRule/RuleAnalyzer.kt | 378 ++ .../legado/app/model/analyzeRule/RuleData.kt | 30 + .../model/analyzeRule/RuleDataInterface.kt | 38 + .../app/model/localBook/BaseLocalBookParse.kt | 21 + .../io/legado/app/model/localBook/EpubFile.kt | 438 ++ .../legado/app/model/localBook/LocalBook.kt | 518 +++ .../io/legado/app/model/localBook/MobiFile.kt | 318 ++ .../io/legado/app/model/localBook/PdfFile.kt | 230 + .../io/legado/app/model/localBook/README.md | 8 + .../io/legado/app/model/localBook/TextFile.kt | 479 ++ .../io/legado/app/model/localBook/UmdFile.kt | 123 + .../io/legado/app/model/remote/RemoteBook.kt | 31 + .../app/model/remote/RemoteBookManager.kt | 38 + .../app/model/remote/RemoteBookWebDav.kt | 90 + .../main/java/io/legado/app/model/rss/Rss.kt | 103 + .../legado/app/model/rss/RssParserByRule.kt | 132 + .../legado/app/model/rss/RssParserDefault.kt | 150 + .../app/model/webBook/BookChapterList.kt | 288 ++ .../legado/app/model/webBook/BookContent.kt | 197 + .../io/legado/app/model/webBook/BookInfo.kt | 171 + .../io/legado/app/model/webBook/BookList.kt | 308 ++ .../legado/app/model/webBook/SearchModel.kt | 211 + .../io/legado/app/model/webBook/WebBook.kt | 415 ++ .../app/receiver/MediaButtonReceiver.kt | 121 + .../app/receiver/NetworkChangedListener.kt | 77 + .../app/receiver/SharedReceiverActivity.kt | 60 + .../app/receiver/TimeBatteryReceiver.kt | 31 + .../io/legado/app/service/AudioPlayService.kt | 659 +++ .../app/service/BaseReadAloudService.kt | 774 ++++ .../io/legado/app/service/CacheBookService.kt | 187 + .../legado/app/service/CheckSourceService.kt | 261 ++ .../io/legado/app/service/DownloadService.kt | 279 ++ .../legado/app/service/ExportBookService.kt | 878 ++++ .../app/service/HttpReadAloudService.kt | 596 +++ .../main/java/io/legado/app/service/README.md | 8 + .../legado/app/service/TTSReadAloudService.kt | 266 ++ .../java/io/legado/app/service/WebService.kt | 230 + .../io/legado/app/service/WebTileService.kt | 79 + app/src/main/java/io/legado/app/ui/README.md | 26 + .../io/legado/app/ui/about/AboutActivity.kt | 62 + .../io/legado/app/ui/about/AboutFragment.kt | 214 + .../io/legado/app/ui/about/AppLogDialog.kt | 90 + .../io/legado/app/ui/about/CrashLogsDialog.kt | 166 + .../legado/app/ui/about/ReadRecordActivity.kt | 235 + .../io/legado/app/ui/about/UpdateDialog.kt | 71 + .../ui/association/AddToBookshelfDialog.kt | 177 + .../association/BaseAssociationViewModel.kt | 48 + .../ui/association/FileAssociationActivity.kt | 219 + .../association/FileAssociationViewModel.kt | 66 + .../ui/association/ImportBookSourceDialog.kt | 288 ++ .../association/ImportBookSourceViewModel.kt | 221 + .../ui/association/ImportDictRuleDialog.kt | 187 + .../ui/association/ImportDictRuleViewModel.kt | 130 + .../app/ui/association/ImportHttpTtsDialog.kt | 187 + .../ui/association/ImportHttpTtsViewModel.kt | 127 + .../ui/association/ImportReplaceRuleDialog.kt | 253 ++ .../association/ImportReplaceRuleViewModel.kt | 141 + .../ui/association/ImportRssSourceDialog.kt | 266 ++ .../association/ImportRssSourceViewModel.kt | 193 + .../app/ui/association/ImportThemeDialog.kt | 177 + .../ui/association/ImportThemeViewModel.kt | 130 + .../ui/association/ImportTxtTocRuleDialog.kt | 177 + .../association/ImportTxtTocRuleViewModel.kt | 131 + .../ui/association/OnLineImportActivity.kt | 115 + .../ui/association/OnLineImportViewModel.kt | 111 + .../ui/association/OpenUrlConfirmActivity.kt | 25 + .../ui/association/OpenUrlConfirmDialog.kt | 132 + .../ui/association/OpenUrlConfirmViewModel.kt | 42 + .../association/VerificationCodeActivity.kt | 29 + .../ui/association/VerificationCodeDialog.kt | 156 + .../association/VerificationCodeViewModel.kt | 37 + .../app/ui/book/audio/AudioPlayActivity.kt | 313 ++ .../app/ui/book/audio/AudioPlayViewModel.kt | 120 + .../app/ui/book/audio/TimerSliderPopup.kt | 55 + .../ui/book/bookmark/AllBookmarkActivity.kt | 82 + .../ui/book/bookmark/AllBookmarkViewModel.kt | 67 + .../app/ui/book/bookmark/BookmarkAdapter.kt | 60 + .../ui/book/bookmark/BookmarkDecoration.kt | 111 + .../app/ui/book/bookmark/BookmarkDialog.kt | 79 + .../legado/app/ui/book/cache/CacheActivity.kt | 581 +++ .../legado/app/ui/book/cache/CacheAdapter.kt | 141 + .../app/ui/book/cache/CacheViewModel.kt | 46 + .../ui/book/changecover/ChangeCoverDialog.kt | 116 + .../book/changecover/ChangeCoverViewModel.kt | 170 + .../app/ui/book/changecover/CoverAdapter.kt | 55 + .../changesource/ChangeBookSourceAdapter.kt | 236 + .../changesource/ChangeBookSourceDialog.kt | 462 ++ .../changesource/ChangeBookSourceViewModel.kt | 580 +++ .../ChangeChapterSourceAdapter.kt | 205 + .../changesource/ChangeChapterSourceDialog.kt | 414 ++ .../ChangeChapterSourceViewModel.kt | 46 + .../changesource/ChangeChapterTocAdapter.kt | 70 + .../ui/book/explore/ExploreShowActivity.kt | 95 + .../app/ui/book/explore/ExploreShowAdapter.kt | 97 + .../ui/book/explore/ExploreShowViewModel.kt | 94 + .../app/ui/book/group/GroupEditDialog.kt | 141 + .../app/ui/book/group/GroupManageDialog.kt | 163 + .../app/ui/book/group/GroupSelectDialog.kt | 167 + .../app/ui/book/group/GroupViewModel.kt | 52 + .../ui/book/import/BaseImportBookActivity.kt | 161 + .../app/ui/book/import/local/ImportBook.kt | 14 + .../book/import/local/ImportBookActivity.kt | 327 ++ .../ui/book/import/local/ImportBookAdapter.kt | 141 + .../book/import/local/ImportBookViewModel.kt | 181 + .../book/import/remote/RemoteBookActivity.kt | 259 ++ .../book/import/remote/RemoteBookAdapter.kt | 157 + .../ui/book/import/remote/RemoteBookSort.kt | 5 + .../book/import/remote/RemoteBookViewModel.kt | 174 + .../book/import/remote/ServerConfigDialog.kt | 131 + .../import/remote/ServerConfigViewModel.kt | 41 + .../ui/book/import/remote/ServersDialog.kt | 167 + .../ui/book/import/remote/ServersViewModel.kt | 17 + .../app/ui/book/info/BookInfoActivity.kt | 771 ++++ .../app/ui/book/info/BookInfoViewModel.kt | 529 +++ .../ui/book/info/edit/BookInfoEditActivity.kt | 171 + .../book/info/edit/BookInfoEditViewModel.kt | 41 + .../legado/app/ui/book/manage/BookAdapter.kt | 239 + .../ui/book/manage/BookshelfManageActivity.kt | 418 ++ .../book/manage/BookshelfManageViewModel.kt | 131 + .../app/ui/book/manage/SourcePickerDialog.kt | 161 + .../app/ui/book/manga/ReadMangaActivity.kt | 856 ++++ .../app/ui/book/manga/ReadMangaViewModel.kt | 298 ++ .../manga/config/MangaColorFilterConfig.kt | 18 + .../manga/config/MangaColorFilterDialog.kt | 79 + .../ui/book/manga/config/MangaEpaperDialog.kt | 50 + .../ui/book/manga/config/MangaFooterConfig.kt | 17 + .../manga/config/MangaFooterSettingDialog.kt | 112 + .../ui/book/manga/entities/BaseMangaPage.kt | 6 + .../manga/entities/EpaperTransformation.kt | 80 + .../manga/entities/GrayscaleTransformation.kt | 57 + .../ui/book/manga/entities/MangaChapter.kt | 9 + .../ui/book/manga/entities/MangaContent.kt | 8 + .../app/ui/book/manga/entities/MangaPage.kt | 10 + .../ui/book/manga/entities/ReaderLoading.kt | 9 + .../GestureDetectorWithLongTap.kt | 64 + .../book/manga/recyclerview/MangaAdapter.kt | 280 ++ .../manga/recyclerview/MangaLayoutManager.kt | 17 + .../app/ui/book/manga/recyclerview/MangaVH.kt | 141 + .../ui/book/manga/recyclerview/ScrollTimer.kt | 75 + .../book/manga/recyclerview/WebtoonFrame.kt | 126 + .../manga/recyclerview/WebtoonRecyclerView.kt | 373 ++ .../app/ui/book/read/BaseReadBookActivity.kt | 392 ++ .../app/ui/book/read/ContentEditDialog.kt | 170 + .../ui/book/read/EffectiveReplacesDialog.kt | 114 + .../io/legado/app/ui/book/read/MangaMenu.kt | 243 + .../app/ui/book/read/ReadBookActivity.kt | 1732 ++++++++ .../app/ui/book/read/ReadBookViewModel.kt | 569 +++ .../io/legado/app/ui/book/read/ReadMenu.kt | 590 +++ .../io/legado/app/ui/book/read/SearchMenu.kt | 234 + .../legado/app/ui/book/read/TextActionMenu.kt | 295 ++ .../app/ui/book/read/config/AutoReadDialog.kt | 131 + .../app/ui/book/read/config/BgAdapter.kt | 49 + .../ui/book/read/config/BgTextConfigDialog.kt | 400 ++ .../ui/book/read/config/ChineseConverter.kt | 53 + .../read/config/ClickActionConfigDialog.kt | 150 + .../ui/book/read/config/HttpTtsEditDialog.kt | 139 + .../book/read/config/HttpTtsEditViewModel.kt | 76 + .../ui/book/read/config/MoreConfigDialog.kt | 211 + .../book/read/config/PaddingConfigDialog.kt | 124 + .../app/ui/book/read/config/PageKeyDialog.kt | 72 + .../book/read/config/ReadAloudConfigDialog.kt | 151 + .../ui/book/read/config/ReadAloudDialog.kt | 232 + .../ui/book/read/config/ReadStyleDialog.kt | 254 ++ .../ui/book/read/config/SpeakEngineDialog.kt | 291 ++ .../book/read/config/SpeakEngineViewModel.kt | 23 + .../read/config/TextFontWeightConverter.kt | 56 + .../ui/book/read/config/TipConfigDialog.kt | 252 ++ .../legado/app/ui/book/read/page/AutoPager.kt | 167 + .../app/ui/book/read/page/ContentTextView.kt | 717 +++ .../legado/app/ui/book/read/page/PageView.kt | 503 +++ .../legado/app/ui/book/read/page/ReadView.kt | 758 ++++ .../app/ui/book/read/page/api/DataSource.kt | 24 + .../app/ui/book/read/page/api/PageFactory.kt | 26 + .../read/page/delegate/CoverPageDelegate.kt | 117 + .../page/delegate/HorizontalPageDelegate.kt | 155 + .../read/page/delegate/NoAnimPageDelegate.kt | 28 + .../book/read/page/delegate/PageDelegate.kt | 208 + .../read/page/delegate/ScrollPageDelegate.kt | 186 + .../page/delegate/SimulationPageDelegate.kt | 613 +++ .../read/page/delegate/SlidePageDelegate.kt | 64 + .../book/read/page/entities/PageDirection.kt | 5 + .../ui/book/read/page/entities/TextChapter.kt | 321 ++ .../ui/book/read/page/entities/TextLine.kt | 274 ++ .../ui/book/read/page/entities/TextPage.kt | 373 ++ .../book/read/page/entities/TextParagraph.kt | 17 + .../app/ui/book/read/page/entities/TextPos.kt | 66 + .../read/page/entities/column/BaseColumn.kt | 21 + .../read/page/entities/column/ButtonColumn.kt | 22 + .../read/page/entities/column/ImageColumn.kt | 52 + .../read/page/entities/column/ReviewColumn.kt | 65 + .../read/page/entities/column/TextColumn.kt | 72 + .../read/page/provider/ChapterProvider.kt | 1053 +++++ .../page/provider/LayoutProgressListener.kt | 22 + .../read/page/provider/TextChapterLayout.kt | 834 ++++ .../ui/book/read/page/provider/TextMeasure.kt | 143 + .../read/page/provider/TextPageFactory.kt | 160 + .../ui/book/read/page/provider/ZhLayout.kt | 278 ++ .../legado/app/ui/book/search/BookAdapter.kt | 46 + .../app/ui/book/search/HistoryKeyAdapter.kt | 55 + .../app/ui/book/search/SearchActivity.kt | 549 +++ .../app/ui/book/search/SearchAdapter.kt | 155 + .../legado/app/ui/book/search/SearchScope.kt | 153 + .../app/ui/book/search/SearchScopeDialog.kt | 255 ++ .../app/ui/book/search/SearchViewModel.kt | 162 + .../searchContent/SearchContentActivity.kt | 258 ++ .../searchContent/SearchContentAdapter.kt | 51 + .../searchContent/SearchContentViewModel.kt | 108 + .../app/ui/book/searchContent/SearchResult.kt | 61 + .../source/debug/BookSourceDebugActivity.kt | 215 + .../source/debug/BookSourceDebugAdapter.kt | 44 + .../book/source/debug/BookSourceDebugModel.kt | 60 + .../source/edit/BookSourceEditActivity.kt | 665 +++ .../book/source/edit/BookSourceEditAdapter.kt | 99 + .../source/edit/BookSourceEditViewModel.kt | 153 + .../book/source/manage/BookSourceActivity.kt | 780 ++++ .../book/source/manage/BookSourceAdapter.kt | 381 ++ .../ui/book/source/manage/BookSourceSort.kt | 5 + .../book/source/manage/BookSourceViewModel.kt | 288 ++ .../book/source/manage/GroupManageDialog.kt | 138 + .../legado/app/ui/book/toc/BookmarkAdapter.kt | 51 + .../app/ui/book/toc/BookmarkFragment.kt | 96 + .../app/ui/book/toc/ChapterListAdapter.kt | 209 + .../app/ui/book/toc/ChapterListFragment.kt | 185 + .../io/legado/app/ui/book/toc/TocActivity.kt | 218 + .../app/ui/book/toc/TocActivityResult.kt | 27 + .../io/legado/app/ui/book/toc/TocViewModel.kt | 136 + .../ui/book/toc/rule/TxtTocRuleActivity.kt | 263 ++ .../app/ui/book/toc/rule/TxtTocRuleAdapter.kt | 233 + .../app/ui/book/toc/rule/TxtTocRuleDialog.kt | 325 ++ .../ui/book/toc/rule/TxtTocRuleEditDialog.kt | 147 + .../ui/book/toc/rule/TxtTocRuleViewModel.kt | 79 + .../legado/app/ui/browser/WebViewActivity.kt | 345 ++ .../io/legado/app/ui/browser/WebViewModel.kt | 142 + .../app/ui/config/BackupConfigFragment.kt | 400 ++ .../legado/app/ui/config/CheckSourceConfig.kt | 104 + .../io/legado/app/ui/config/ConfigActivity.kt | 51 + .../java/io/legado/app/ui/config/ConfigTag.kt | 11 + .../legado/app/ui/config/ConfigViewModel.kt | 51 + .../app/ui/config/CoverConfigFragment.kt | 190 + .../app/ui/config/CoverRuleConfigDialog.kt | 68 + .../app/ui/config/DirectLinkUploadConfig.kt | 130 + .../app/ui/config/OtherConfigFragment.kt | 336 ++ .../app/ui/config/ThemeConfigFragment.kt | 359 ++ .../legado/app/ui/config/ThemeListDialog.kt | 121 + .../app/ui/config/WelcomeConfigFragment.kt | 213 + .../java/io/legado/app/ui/dict/DictDialog.kt | 91 + .../io/legado/app/ui/dict/DictViewModel.kt | 37 + .../app/ui/dict/rule/DictRuleActivity.kt | 241 + .../app/ui/dict/rule/DictRuleAdapter.kt | 208 + .../app/ui/dict/rule/DictRuleEditDialog.kt | 122 + .../app/ui/dict/rule/DictRuleViewModel.kt | 64 + .../legado/app/ui/file/FileManageActivity.kt | 232 + .../legado/app/ui/file/FileManageViewModel.kt | 52 + .../io/legado/app/ui/file/FilePickerDialog.kt | 295 ++ .../legado/app/ui/file/FilePickerViewModel.kt | 76 + .../legado/app/ui/file/HandleFileActivity.kt | 331 ++ .../legado/app/ui/file/HandleFileContract.kt | 89 + .../legado/app/ui/file/HandleFileViewModel.kt | 63 + .../app/ui/file/utils/FilePickerIcon.java | 283 ++ .../java/io/legado/app/ui/font/FontAdapter.kt | 72 + .../io/legado/app/ui/font/FontSelectDialog.kt | 209 + .../app/ui/login/SourceLoginActivity.kt | 36 + .../legado/app/ui/login/SourceLoginDialog.kt | 198 + .../app/ui/login/SourceLoginViewModel.kt | 43 + .../app/ui/login/WebViewLoginFragment.kt | 157 + .../io/legado/app/ui/main/MainActivity.kt | 466 ++ .../app/ui/main/MainFragmentInterface.kt | 7 + .../io/legado/app/ui/main/MainViewModel.kt | 249 ++ .../main/bookshelf/BaseBookshelfFragment.kt | 263 ++ .../ui/main/bookshelf/BookshelfViewModel.kt | 188 + .../bookshelf/style1/BookshelfFragment1.kt | 188 + .../style1/books/BaseBooksAdapter.kt | 95 + .../style1/books/BooksAdapterGrid.kt | 75 + .../style1/books/BooksAdapterList.kt | 110 + .../bookshelf/style1/books/BooksFragment.kt | 268 ++ .../main/bookshelf/style2/BaseBooksAdapter.kt | 143 + .../main/bookshelf/style2/BooksAdapterGrid.kt | 142 + .../main/bookshelf/style2/BooksAdapterList.kt | 158 + .../bookshelf/style2/BookshelfFragment2.kt | 261 ++ .../app/ui/main/explore/ExploreAdapter.kt | 193 + .../main/explore/ExploreDiffItemCallBack.kt | 17 + .../app/ui/main/explore/ExploreFragment.kt | 240 + .../app/ui/main/explore/ExploreViewModel.kt | 26 + .../io/legado/app/ui/main/my/MyFragment.kt | 174 + .../io/legado/app/ui/main/rss/RssAdapter.kt | 86 + .../io/legado/app/ui/main/rss/RssFragment.kt | 222 + .../io/legado/app/ui/main/rss/RssViewModel.kt | 83 + .../io/legado/app/ui/qrcode/QrCodeActivity.kt | 57 + .../io/legado/app/ui/qrcode/QrCodeFragment.kt | 33 + .../io/legado/app/ui/qrcode/QrCodeResult.kt | 23 + .../app/ui/qrcode/ScanResultCallback.kt | 9 + .../app/ui/replace/GroupManageDialog.kt | 144 + .../app/ui/replace/ReplaceRuleActivity.kt | 361 ++ .../app/ui/replace/ReplaceRuleAdapter.kt | 234 + .../app/ui/replace/ReplaceRuleViewModel.kt | 135 + .../ui/replace/edit/ReplaceEditActivity.kt | 159 + .../ui/replace/edit/ReplaceEditViewModel.kt | 69 + .../ui/rss/article/BaseRssArticlesAdapter.kt | 16 + .../app/ui/rss/article/ReadRecordDialog.kt | 88 + .../app/ui/rss/article/RssArticlesAdapter.kt | 91 + .../app/ui/rss/article/RssArticlesAdapter1.kt | 91 + .../app/ui/rss/article/RssArticlesAdapter2.kt | 91 + .../app/ui/rss/article/RssArticlesFragment.kt | 158 + .../ui/rss/article/RssArticlesViewModel.kt | 96 + .../app/ui/rss/article/RssSortActivity.kt | 168 + .../app/ui/rss/article/RssSortViewModel.kt | 94 + .../ui/rss/favorites/RssFavoritesActivity.kt | 184 + .../ui/rss/favorites/RssFavoritesAdapter.kt | 91 + .../ui/rss/favorites/RssFavoritesDialog.kt | 74 + .../ui/rss/favorites/RssFavoritesFragment.kt | 87 + .../ui/rss/favorites/RssFavoritesViewModel.kt | 9 + .../legado/app/ui/rss/read/ReadRssActivity.kt | 532 +++ .../app/ui/rss/read/ReadRssViewModel.kt | 251 ++ .../legado/app/ui/rss/read/RssJsExtensions.kt | 24 + .../legado/app/ui/rss/read/VisibleWebView.kt | 17 + .../source/debug/RssSourceDebugActivity.kt | 78 + .../rss/source/debug/RssSourceDebugAdapter.kt | 44 + .../rss/source/debug/RssSourceDebugModel.kt | 50 + .../rss/source/edit/RssSourceEditActivity.kt | 407 ++ .../rss/source/edit/RssSourceEditAdapter.kt | 131 + .../rss/source/edit/RssSourceEditViewModel.kt | 119 + .../ui/rss/source/manage/GroupManageDialog.kt | 148 + .../ui/rss/source/manage/RssSourceActivity.kt | 414 ++ .../ui/rss/source/manage/RssSourceAdapter.kt | 247 + .../rss/source/manage/RssSourceViewModel.kt | 168 + .../ui/rss/subscription/RuleSubActivity.kt | 148 + .../app/ui/rss/subscription/RuleSubAdapter.kt | 97 + .../legado/app/ui/welcome/WelcomeActivity.kt | 95 + .../io/legado/app/ui/widget/BatteryView.kt | 122 + .../io/legado/app/ui/widget/DetailSeekBar.kt | 92 + .../java/io/legado/app/ui/widget/LabelsBar.kt | 62 + .../widget/NoChildScrollNestedScrollView.kt | 29 + .../io/legado/app/ui/widget/PopupAction.kt | 69 + .../legado/app/ui/widget/ReaderInfoBarView.kt | 194 + .../io/legado/app/ui/widget/SearchView.kt | 110 + .../legado/app/ui/widget/SelectActionBar.kt | 128 + .../io/legado/app/ui/widget/ShadowLayout.kt | 185 + .../java/io/legado/app/ui/widget/TitleBar.kt | 274 ++ .../app/ui/widget/anima/RefreshProgressBar.kt | 195 + .../app/ui/widget/anima/RotateLoading.kt | 228 + .../explosion_field/ExplosionAnimator.kt | 152 + .../anima/explosion_field/ExplosionField.kt | 21 + .../anima/explosion_field/ExplosionView.kt | 156 + .../explosion_field/OnAnimatorListener.kt | 8 + .../ui/widget/anima/explosion_field/Utils.kt | 80 + .../app/ui/widget/checkbox/SmoothCheckBox.kt | 327 ++ .../io/legado/app/ui/widget/code/CodeView.kt | 412 ++ .../app/ui/widget/code/CodeViewExtensions.kt | 35 + .../app/ui/widget/code/KeywordTokenizer.kt | 25 + .../legado/app/ui/widget/dialog/CodeDialog.kt | 77 + .../app/ui/widget/dialog/PhotoDialog.kt | 71 + .../legado/app/ui/widget/dialog/TextDialog.kt | 125 + .../app/ui/widget/dialog/TextListDialog.kt | 81 + .../app/ui/widget/dialog/UrlOptionDialog.kt | 51 + .../app/ui/widget/dialog/VariableDialog.kt | 96 + .../legado/app/ui/widget/dialog/WaitDialog.kt | 28 + .../dynamiclayout/DynamicFrameLayout.kt | 192 + .../ui/widget/dynamiclayout/ViewSwitcher.kt | 29 + .../io/legado/app/ui/widget/image/ArcView.kt | 87 + .../app/ui/widget/image/CircleImageView.kt | 450 ++ .../app/ui/widget/image/CoverImageView.kt | 254 ++ .../app/ui/widget/image/FilletImageView.kt | 99 + .../legado/app/ui/widget/image/ImageButton.kt | 20 + .../legado/app/ui/widget/image/PhotoView.kt | 1260 ++++++ .../legado/app/ui/widget/image/photo/Info.kt | 49 + .../image/photo/RotateGestureDetector.kt | 54 + .../widget/keyboard/KeyboardAssistsConfig.kt | 181 + .../app/ui/widget/keyboard/KeyboardToolPop.kt | 172 + .../ui/widget/number/NumberPickerDialog.kt | 76 + .../app/ui/widget/recycler/DividerNoLast.kt | 170 + .../widget/recycler/DragSelectTouchHelper.kt | 1001 +++++ .../recycler/HeaderAdapterDataObserver.kt | 34 + .../ui/widget/recycler/ItemTouchCallback.kt | 148 + .../app/ui/widget/recycler/LoadMoreView.kt | 107 + .../NoChildScrollLinearLayoutManager.kt | 25 + .../widget/recycler/RecyclerViewAtPager2.kt | 48 + .../widget/recycler/UpLinearLayoutManager.kt | 46 + .../app/ui/widget/recycler/VerticalDivider.kt | 16 + .../ui/widget/recycler/ViewPager2Container.kt | 122 + .../scroller/FastScrollRecyclerView.kt | 175 + .../scroller/FastScrollStateChangeListener.kt | 14 + .../widget/recycler/scroller/FastScroller.kt | 549 +++ .../widget/seekbar/SeekBarChangeListener.kt | 19 + .../app/ui/widget/seekbar/VerticalSeekBar.kt | 346 ++ .../widget/seekbar/VerticalSeekBarWrapper.kt | 184 + .../app/ui/widget/text/AccentBgTextView.kt | 52 + .../ui/widget/text/AccentStrokeTextView.kt | 60 + .../app/ui/widget/text/AccentTextView.kt | 21 + .../ui/widget/text/AutoCompleteTextView.kt | 80 + .../io/legado/app/ui/widget/text/BadgeView.kt | 236 + .../app/ui/widget/text/BevelLabelView.kt | 352 ++ .../legado/app/ui/widget/text/EditEntity.kt | 31 + .../app/ui/widget/text/MultilineTextView.kt | 28 + .../app/ui/widget/text/PrimaryTextView.kt | 18 + .../text/ScrollMultiAutoCompleteTextView.kt | 275 ++ .../app/ui/widget/text/ScrollTextView.kt | 272 ++ .../app/ui/widget/text/SecondaryTextView.kt | 18 + .../app/ui/widget/text/StrokeTextView.kt | 89 + .../app/ui/widget/text/TextInputLayout.kt | 18 + .../main/java/io/legado/app/utils/ACache.kt | 790 ++++ .../io/legado/app/utils/ActivityExtensions.kt | 254 ++ .../io/legado/app/utils/ActivityResult.kt | 47 + .../app/utils/ActivityResultContracts.kt | 72 + .../io/legado/app/utils/AlphanumComparator.kt | 81 + .../legado/app/utils/AnimationExtensions.kt | 15 + .../java/io/legado/app/utils/ArchiveUtils.kt | 110 + .../io/legado/app/utils/AsyncFileHandler.kt | 18 + .../java/io/legado/app/utils/BitmapUtils.kt | 270 ++ .../legado/app/utils/BookChapterExtensions.kt | 8 + .../legado/app/utils/ByteArrayExtensions.kt | 45 + .../java/io/legado/app/utils/ChineseUtils.kt | 51 + .../legado/app/utils/CollectionExtensions.kt | 60 + .../java/io/legado/app/utils/ColorUtils.kt | 119 + .../app/utils/ConfigurationExtensions.kt | 14 + .../io/legado/app/utils/ConflateLiveData.kt | 37 + .../io/legado/app/utils/ConstraintModify.kt | 298 ++ .../io/legado/app/utils/ContextExtensions.kt | 417 ++ .../io/legado/app/utils/ConvertExtensions.kt | 131 + .../app/utils/CookieManagerExtensions.kt | 22 + .../legado/app/utils/CoroutineExtensions.kt | 37 + .../io/legado/app/utils/CustomExportUtils.kt | 27 + .../main/java/io/legado/app/utils/Debounce.kt | 129 + .../main/java/io/legado/app/utils/DebugLog.kt | 54 + .../io/legado/app/utils/DialogExtensions.kt | 83 + .../java/io/legado/app/utils/DocumentUtils.kt | 46 + .../java/io/legado/app/utils/DrawableUtils.kt | 55 + .../java/io/legado/app/utils/EncoderUtils.kt | 51 + .../io/legado/app/utils/EncodingDetect.kt | 98 + .../io/legado/app/utils/EventBusExtensions.kt | 98 + .../io/legado/app/utils/FileDocExtensions.kt | 426 ++ .../io/legado/app/utils/FileExtensions.kt | 88 + .../java/io/legado/app/utils/FileUtils.kt | 763 ++++ .../io/legado/app/utils/FlowExtensions.kt | 263 ++ .../io/legado/app/utils/FragmentExtensions.kt | 114 + .../io/legado/app/utils/GsonExtensions.kt | 244 + .../java/io/legado/app/utils/HandlerUtils.kt | 50 + .../java/io/legado/app/utils/HtmlFormatter.kt | 71 + .../java/io/legado/app/utils/ImageUtils.kt | 74 + .../legado/app/utils/InputStreamExtensions.kt | 25 + .../io/legado/app/utils/IntentExtensions.kt | 21 + .../java/io/legado/app/utils/IntentType.kt | 31 + .../main/java/io/legado/app/utils/JsURL.kt | 44 + .../io/legado/app/utils/JsonExtensions.kt | 20 + .../io/legado/app/utils/JsoupExtensions.kt | 78 + .../main/java/io/legado/app/utils/LogUtils.kt | 144 + .../main/java/io/legado/app/utils/MD5Utils.kt | 34 + .../java/io/legado/app/utils/MapExtensions.kt | 32 + .../io/legado/app/utils/MenuExtensions.kt | 113 + .../io/legado/app/utils/MenuItemExtensions.kt | 11 + .../app/utils/MutableLiveDataExtensions.kt | 13 + .../legado/app/utils/NavigationViewUtils.kt | 42 + .../java/io/legado/app/utils/NetworkUtils.kt | 298 ++ .../io/legado/app/utils/PaintExtensions.kt | 27 + .../app/utils/ParcelFileDescriptorChannel.kt | 80 + .../legado/app/utils/PreferencesExtensions.kt | 131 + .../java/io/legado/app/utils/QRCodeUtils.kt | 479 ++ .../java/io/legado/app/utils/RandomColor.kt | 61 + .../java/io/legado/app/utils/RealPathUtil.kt | 185 + .../app/utils/RecyclerViewExtensions.kt | 30 + .../io/legado/app/utils/RegexExtensions.kt | 78 + .../app/utils/RequestManagerExtensions.kt | 30 + .../java/io/legado/app/utils/Snackbars.kt | 169 + .../io/legado/app/utils/StringExtensions.kt | 146 + .../java/io/legado/app/utils/StringUtils.kt | 370 ++ .../main/java/io/legado/app/utils/SvgUtils.kt | 85 + .../io/legado/app/utils/SyncedRenderer.kt | 25 + .../java/io/legado/app/utils/SystemUtils.kt | 54 + .../main/java/io/legado/app/utils/Throttle.kt | 16 + .../legado/app/utils/ThrowableExtensions.kt | 19 + .../java/io/legado/app/utils/TimeUtils.kt | 52 + .../java/io/legado/app/utils/ToastUtils.kt | 86 + .../io/legado/app/utils/ToolBarExtensions.kt | 23 + .../java/io/legado/app/utils/UriExtensions.kt | 316 ++ .../main/java/io/legado/app/utils/UrlUtil.kt | 171 + .../java/io/legado/app/utils/Utf8BomUtils.kt | 38 + .../io/legado/app/utils/ViewExtensions.kt | 321 ++ .../legado/app/utils/WebSettingsExtensions.kt | 39 + .../app/utils/WindowInsetsExtensions.kt | 9 + .../canvasrecorder/BaseCanvasRecorder.kt | 36 + .../utils/canvasrecorder/CanvasRecorder.kt | 27 + .../canvasrecorder/CanvasRecorderApi23Impl.kt | 47 + .../canvasrecorder/CanvasRecorderApi29Impl.kt | 76 + .../CanvasRecorderExtensions.kt | 44 + .../canvasrecorder/CanvasRecorderFactory.kt | 27 + .../canvasrecorder/CanvasRecorderImpl.kt | 70 + .../canvasrecorder/CanvasRecorderLocked.kt | 64 + .../utils/canvasrecorder/pools/CanvasPool.kt | 21 + .../utils/canvasrecorder/pools/PicturePool.kt | 10 + .../canvasrecorder/pools/RenderNodePool.kt | 18 + .../app/utils/compress/LibArchiveUtils.kt | 372 ++ .../io/legado/app/utils/compress/ZipUtils.kt | 410 ++ .../app/utils/objectpool/BaseObjectPool.kt | 19 + .../utils/objectpool/BaseSafeObjectPool.kt | 9 + .../legado/app/utils/objectpool/ObjectPool.kt | 11 + .../utils/objectpool/ObjectPoolExtensions.kt | 3 + .../app/utils/objectpool/ObjectPoolLocked.kt | 15 + .../ActivityViewBindings.kt | 22 + .../FragmentViewBindings.kt | 54 + .../ViewBindingProperty.kt | 57 + .../main/java/io/legado/app/web/HttpServer.kt | 152 + app/src/main/java/io/legado/app/web/ReadMe.md | 5 + .../java/io/legado/app/web/WebSocketServer.kt | 24 + .../app/web/socket/BookSearchWebSocket.kt | 97 + .../web/socket/BookSourceDebugWebSocket.kt | 102 + .../app/web/socket/RssSourceDebugWebSocket.kt | 97 + .../java/io/legado/app/web/utils/AssetsWeb.kt | 45 + app/src/main/res/anim/anim_none.xml | 4 + .../main/res/anim/anim_readbook_bottom_in.xml | 7 + .../res/anim/anim_readbook_bottom_out.xml | 7 + .../main/res/anim/anim_readbook_top_in.xml | 7 + .../main/res/anim/anim_readbook_top_out.xml | 7 + app/src/main/res/color/selector_image.xml | 7 + .../res/drawable/bg_chapter_item_divider.xml | 10 + app/src/main/res/drawable/bg_edit.xml | 7 + .../res/drawable/bg_eink_border_bottom.xml | 16 + .../res/drawable/bg_eink_border_dialog.xml | 8 + .../main/res/drawable/bg_eink_border_top.xml | 16 + .../main/res/drawable/bg_find_book_group.xml | 7 + app/src/main/res/drawable/bg_gradient.xml | 8 + app/src/main/res/drawable/bg_img_border.xml | 13 + .../res/drawable/bg_item_focused_on_tv.xml | 4 + app/src/main/res/drawable/bg_popup_menu.xml | 9 + app/src/main/res/drawable/bg_prefs_color.xml | 5 + app/src/main/res/drawable/bg_searchview.xml | 11 + .../main/res/drawable/bg_shadow_bottom.xml | 8 + .../res/drawable/bg_shadow_bottom_night.xml | 8 + app/src/main/res/drawable/bg_shadow_top.xml | 8 + .../main/res/drawable/bg_shadow_top_night.xml | 8 + .../main/res/drawable/bg_textfield_search.xml | 7 + .../main/res/drawable/fastscroll_bubble.xml | 37 + .../main/res/drawable/fastscroll_handle.xml | 29 + .../main/res/drawable/fastscroll_track.xml | 25 + app/src/main/res/drawable/ic_add.xml | 11 + app/src/main/res/drawable/ic_add_online.xml | 11 + app/src/main/res/drawable/ic_arrange.xml | 17 + app/src/main/res/drawable/ic_arrow_back.xml | 9 + app/src/main/res/drawable/ic_arrow_down.xml | 9 + .../main/res/drawable/ic_arrow_drop_down.xml | 9 + .../main/res/drawable/ic_arrow_drop_up.xml | 9 + app/src/main/res/drawable/ic_arrow_right.xml | 9 + app/src/main/res/drawable/ic_author.xml | 12 + app/src/main/res/drawable/ic_auto_page.xml | 14 + .../main/res/drawable/ic_auto_page_stop.xml | 17 + app/src/main/res/drawable/ic_backup.xml | 9 + .../main/res/drawable/ic_baseline_close.xml | 10 + .../main/res/drawable/ic_baseline_sort_24.xml | 5 + app/src/main/res/drawable/ic_book_has.xml | 14 + app/src/main/res/drawable/ic_book_last.xml | 15 + app/src/main/res/drawable/ic_bookmark.xml | 11 + app/src/main/res/drawable/ic_bottom_books.xml | 5 + .../main/res/drawable/ic_bottom_books_e.xml | 12 + .../main/res/drawable/ic_bottom_books_s.xml | 12 + .../main/res/drawable/ic_bottom_explore.xml | 5 + .../main/res/drawable/ic_bottom_explore_e.xml | 12 + .../main/res/drawable/ic_bottom_explore_s.xml | 9 + .../main/res/drawable/ic_bottom_person.xml | 5 + .../main/res/drawable/ic_bottom_person_e.xml | 12 + .../main/res/drawable/ic_bottom_person_s.xml | 12 + .../main/res/drawable/ic_bottom_rss_feed.xml | 5 + .../res/drawable/ic_bottom_rss_feed_e.xml | 12 + .../res/drawable/ic_bottom_rss_feed_s.xml | 9 + app/src/main/res/drawable/ic_brightness.xml | 11 + .../main/res/drawable/ic_brightness_auto.xml | 11 + app/src/main/res/drawable/ic_bubble_chart.xml | 25 + app/src/main/res/drawable/ic_bug_report.xml | 9 + app/src/main/res/drawable/ic_cfg_about.xml | 15 + app/src/main/res/drawable/ic_cfg_backup.xml | 9 + app/src/main/res/drawable/ic_cfg_donate.xml | 12 + app/src/main/res/drawable/ic_cfg_other.xml | 12 + app/src/main/res/drawable/ic_cfg_replace.xml | 12 + app/src/main/res/drawable/ic_cfg_source.xml | 12 + app/src/main/res/drawable/ic_cfg_theme.xml | 9 + app/src/main/res/drawable/ic_cfg_web.xml | 9 + app/src/main/res/drawable/ic_chapter_list.xml | 20 + app/src/main/res/drawable/ic_check.xml | 9 + app/src/main/res/drawable/ic_check_source.xml | 14 + app/src/main/res/drawable/ic_clear_all.xml | 17 + app/src/main/res/drawable/ic_copy.xml | 17 + .../res/drawable/ic_create_folder_outline.xml | 10 + app/src/main/res/drawable/ic_cursor_left.xml | 11 + app/src/main/res/drawable/ic_cursor_right.xml | 11 + app/src/main/res/drawable/ic_daytime.xml | 35 + app/src/main/res/drawable/ic_divider.xml | 6 + app/src/main/res/drawable/ic_download.xml | 9 + .../main/res/drawable/ic_download_line.xml | 14 + app/src/main/res/drawable/ic_edit.xml | 14 + app/src/main/res/drawable/ic_exchange.xml | 17 + .../main/res/drawable/ic_exchange_order.xml | 12 + app/src/main/res/drawable/ic_exit.xml | 11 + app/src/main/res/drawable/ic_expand_less.xml | 9 + app/src/main/res/drawable/ic_expand_more.xml | 9 + app/src/main/res/drawable/ic_export.xml | 14 + app/src/main/res/drawable/ic_fast_forward.xml | 9 + app/src/main/res/drawable/ic_fast_rewind.xml | 9 + app/src/main/res/drawable/ic_find_replace.xml | 14 + app/src/main/res/drawable/ic_folder.xml | 10 + app/src/main/res/drawable/ic_folder_open.xml | 9 + .../main/res/drawable/ic_folder_outline.xml | 10 + app/src/main/res/drawable/ic_groups.xml | 20 + app/src/main/res/drawable/ic_help.xml | 12 + app/src/main/res/drawable/ic_history.xml | 12 + app/src/main/res/drawable/ic_image.xml | 14 + app/src/main/res/drawable/ic_import.xml | 15 + .../res/drawable/ic_interface_setting.xml | 14 + app/src/main/res/drawable/ic_launcher1.xml | 69 + app/src/main/res/drawable/ic_launcher1_b.xml | 56 + app/src/main/res/drawable/ic_launcher2.xml | 76 + app/src/main/res/drawable/ic_launcher3.xml | 147 + app/src/main/res/drawable/ic_launcher4.xml | 18 + app/src/main/res/drawable/ic_launcher4_b.xml | 9 + app/src/main/res/drawable/ic_launcher5.xml | 57 + app/src/main/res/drawable/ic_launcher5_b.xml | 10 + app/src/main/res/drawable/ic_launcher6.xml | 18 + app/src/main/res/drawable/ic_launcher7.xml | 57 + app/src/main/res/drawable/ic_launcher7_b.xml | 9 + app/src/main/res/drawable/ic_lock_outline.xml | 1 + app/src/main/res/drawable/ic_menu.xml | 9 + app/src/main/res/drawable/ic_more.xml | 15 + app/src/main/res/drawable/ic_more_vert.xml | 9 + .../main/res/drawable/ic_network_check.xml | 9 + .../main/res/drawable/ic_outline_cloud_24.xml | 10 + .../main/res/drawable/ic_outline_delete.xml | 10 + app/src/main/res/drawable/ic_pause_24dp.xml | 9 + .../res/drawable/ic_pause_outline_24dp.xml | 14 + app/src/main/res/drawable/ic_play_24dp.xml | 9 + .../drawable/ic_play_mode_list_end_stop.xml | 12 + .../res/drawable/ic_play_mode_list_loop.xml | 12 + .../main/res/drawable/ic_play_mode_random.xml | 15 + .../res/drawable/ic_play_mode_single_loop.xml | 18 + .../res/drawable/ic_play_outline_24dp.xml | 11 + app/src/main/res/drawable/ic_praise.xml | 7 + app/src/main/res/drawable/ic_read_aloud.xml | 11 + app/src/main/res/drawable/ic_reduce.xml | 9 + .../res/drawable/ic_refresh_black_24dp.xml | 14 + .../res/drawable/ic_refresh_white_24dp.xml | 9 + app/src/main/res/drawable/ic_restore.xml | 9 + app/src/main/res/drawable/ic_save.xml | 14 + app/src/main/res/drawable/ic_scan.xml | 14 + app/src/main/res/drawable/ic_scoring.xml | 11 + app/src/main/res/drawable/ic_screen.xml | 11 + app/src/main/res/drawable/ic_search.xml | 17 + app/src/main/res/drawable/ic_search_hint.xml | 8 + app/src/main/res/drawable/ic_settings.xml | 16 + app/src/main/res/drawable/ic_share.xml | 11 + app/src/main/res/drawable/ic_skip_next.xml | 9 + .../main/res/drawable/ic_skip_previous.xml | 9 + app/src/main/res/drawable/ic_sort.xml | 10 + app/src/main/res/drawable/ic_star.xml | 9 + app/src/main/res/drawable/ic_star_border.xml | 9 + .../main/res/drawable/ic_stop_black_24dp.xml | 9 + .../res/drawable/ic_storage_black_24dp.xml | 9 + app/src/main/res/drawable/ic_swap_horiz.xml | 10 + .../main/res/drawable/ic_time_add_24dp.xml | 12 + .../main/res/drawable/ic_timer_black_24dp.xml | 20 + app/src/main/res/drawable/ic_toc.xml | 9 + app/src/main/res/drawable/ic_translate.xml | 14 + app/src/main/res/drawable/ic_update.xml | 14 + app/src/main/res/drawable/ic_view_quilt.xml | 26 + .../main/res/drawable/ic_visibility_off.xml | 9 + app/src/main/res/drawable/ic_volume_up.xml | 9 + app/src/main/res/drawable/ic_web_outline.xml | 13 + .../main/res/drawable/ic_web_service_noti.xml | 10 + app/src/main/res/drawable/icon_read_book.png | Bin 0 -> 2716 bytes .../main/res/drawable/image_cover_default.jpg | Bin 0 -> 51572 bytes app/src/main/res/drawable/image_legado.png | Bin 0 -> 7656 bytes .../main/res/drawable/image_loading_error.png | Bin 0 -> 6933 bytes app/src/main/res/drawable/image_rss.jpg | Bin 0 -> 8595 bytes .../main/res/drawable/image_rss_article.jpg | Bin 0 -> 13793 bytes .../recyclerview_divider_horizontal.xml | 9 + .../recyclerview_divider_vertical.xml | 12 + .../res/drawable/selector_btn_accent_bg.xml | 5 + .../res/drawable/selector_circle_btn_bg.xml | 4 + .../main/res/drawable/selector_common_bg.xml | 7 + .../res/drawable/selector_fillet_btn_bg.xml | 5 + .../main/res/drawable/selector_tv_black.xml | 6 + app/src/main/res/drawable/shape_card_view.xml | 6 + app/src/main/res/drawable/shape_circle.xml | 11 + .../main/res/drawable/shape_fillet_btn.xml | 6 + .../res/drawable/shape_fillet_btn_press.xml | 6 + .../drawable/shape_pop_checkaddshelf_bg.xml | 6 + .../main/res/drawable/shape_radius_10dp.xml | 8 + .../main/res/drawable/shape_radius_1dp.xml | 8 + .../main/res/drawable/shape_space_divider.xml | 8 + .../main/res/drawable/shape_text_cursor.xml | 7 + .../res/drawable/shape_translucent_card.xml | 6 + .../res/layout-land/activity_book_info.xml | 423 ++ app/src/main/res/layout/activity_about.xml | 47 + .../main/res/layout/activity_all_bookmark.xml | 23 + .../main/res/layout/activity_arrange_book.xml | 31 + .../main/res/layout/activity_audio_play.xml | 267 ++ .../main/res/layout/activity_book_info.xml | 422 ++ .../res/layout/activity_book_info_edit.xml | 159 + .../main/res/layout/activity_book_read.xml | 53 + .../main/res/layout/activity_book_search.xml | 109 + .../main/res/layout/activity_book_source.xml | 35 + .../res/layout/activity_book_source_edit.xml | 92 + .../main/res/layout/activity_cache_book.xml | 20 + .../main/res/layout/activity_chapter_list.xml | 22 + app/src/main/res/layout/activity_config.xml | 21 + .../main/res/layout/activity_dict_rule.xml | 34 + app/src/main/res/layout/activity_donate.xml | 21 + .../main/res/layout/activity_explore_show.xml | 35 + .../main/res/layout/activity_file_manage.xml | 54 + .../main/res/layout/activity_import_book.xml | 95 + app/src/main/res/layout/activity_main.xml | 26 + app/src/main/res/layout/activity_manga.xml | 98 + .../res/layout/activity_qrcode_capture.xml | 19 + .../main/res/layout/activity_read_record.xml | 68 + .../main/res/layout/activity_replace_edit.xml | 171 + .../main/res/layout/activity_replace_rule.xml | 36 + .../main/res/layout/activity_rss_artivles.xml | 26 + .../res/layout/activity_rss_favorites.xml | 27 + app/src/main/res/layout/activity_rss_read.xml | 41 + .../main/res/layout/activity_rss_source.xml | 35 + .../res/layout/activity_rss_source_edit.xml | 74 + app/src/main/res/layout/activity_rule_sub.xml | 44 + .../res/layout/activity_search_content.xml | 96 + .../main/res/layout/activity_source_debug.xml | 207 + .../main/res/layout/activity_source_login.xml | 9 + .../main/res/layout/activity_translucence.xml | 20 + .../main/res/layout/activity_txt_toc_rule.xml | 32 + app/src/main/res/layout/activity_web_view.xml | 41 + app/src/main/res/layout/activity_welcome.xml | 82 + .../res/layout/dialog_add_to_bookshelf.xml | 124 + app/src/main/res/layout/dialog_auto_read.xml | 213 + .../res/layout/dialog_book_change_source.xml | 86 + .../res/layout/dialog_book_group_edit.xml | 131 + .../res/layout/dialog_book_group_picker.xml | 51 + app/src/main/res/layout/dialog_bookmark.xml | 121 + .../res/layout/dialog_bookshelf_config.xml | 187 + .../main/res/layout/dialog_change_cover.xml | 29 + .../layout/dialog_chapter_change_source.xml | 131 + .../res/layout/dialog_check_source_config.xml | 101 + .../res/layout/dialog_click_action_config.xml | 167 + app/src/main/res/layout/dialog_code_view.xml | 26 + .../main/res/layout/dialog_content_edit.xml | 38 + .../res/layout/dialog_cover_rule_config.xml | 109 + .../main/res/layout/dialog_custom_group.xml | 72 + app/src/main/res/layout/dialog_dict.xml | 34 + .../main/res/layout/dialog_dict_rule_edit.xml | 86 + .../dialog_direct_link_upload_config.xml | 122 + .../res/layout/dialog_download_choice.xml | 83 + app/src/main/res/layout/dialog_edit_text.xml | 16 + .../main/res/layout/dialog_file_chooser.xml | 55 + .../main/res/layout/dialog_font_select.xml | 21 + .../main/res/layout/dialog_http_tts_edit.xml | 153 + .../main/res/layout/dialog_image_blurring.xml | 30 + app/src/main/res/layout/dialog_login.xml | 51 + .../res/layout/dialog_manga_color_filter.xml | 57 + .../main/res/layout/dialog_manga_epaper.xml | 29 + .../layout/dialog_manga_footer_setting.xml | 174 + .../res/layout/dialog_multiple_edit_text.xml | 44 + .../main/res/layout/dialog_number_picker.xml | 13 + .../res/layout/dialog_open_url_confirm.xml | 48 + app/src/main/res/layout/dialog_page_key.xml | 94 + app/src/main/res/layout/dialog_photo_view.xml | 15 + .../res/layout/dialog_progressbar_view.xml | 36 + app/src/main/res/layout/dialog_read_aloud.xml | 409 ++ .../main/res/layout/dialog_read_bg_text.xml | 185 + .../res/layout/dialog_read_book_style.xml | 309 ++ .../main/res/layout/dialog_read_padding.xml | 172 + .../main/res/layout/dialog_recycler_view.xml | 96 + .../res/layout/dialog_rss_favorite_config.xml | 112 + .../main/res/layout/dialog_rule_sub_edit.xml | 56 + .../main/res/layout/dialog_search_scope.xml | 114 + .../layout/dialog_select_section_export.xml | 116 + .../res/layout/dialog_simulated_reading.xml | 185 + .../main/res/layout/dialog_source_picker.xml | 99 + app/src/main/res/layout/dialog_text_view.xml | 39 + app/src/main/res/layout/dialog_tip_config.xml | 353 ++ app/src/main/res/layout/dialog_toc_regex.xml | 53 + .../main/res/layout/dialog_toc_regex_edit.xml | 86 + app/src/main/res/layout/dialog_update.xml | 24 + .../res/layout/dialog_url_option_edit.xml | 184 + app/src/main/res/layout/dialog_variable.xml | 74 + .../layout/dialog_verification_code_view.xml | 43 + app/src/main/res/layout/dialog_wait.xml | 25 + .../main/res/layout/dialog_webdav_server.xml | 89 + app/src/main/res/layout/fragment_bookmark.xml | 14 + app/src/main/res/layout/fragment_books.xml | 32 + .../main/res/layout/fragment_bookshelf1.xml | 21 + .../main/res/layout/fragment_bookshelf2.xml | 47 + .../main/res/layout/fragment_chapter_list.xml | 66 + app/src/main/res/layout/fragment_explore.xml | 40 + .../main/res/layout/fragment_my_config.xml | 21 + app/src/main/res/layout/fragment_rss.xml | 42 + .../main/res/layout/fragment_rss_articles.xml | 13 + .../res/layout/fragment_web_view_login.xml | 30 + app/src/main/res/layout/item_1line_text.xml | 20 + .../res/layout/item_1line_text_and_del.xml | 29 + app/src/main/res/layout/item_app_log.xml | 22 + app/src/main/res/layout/item_arrange_book.xml | 102 + app/src/main/res/layout/item_bg_image.xml | 25 + .../main/res/layout/item_book_file_import.xml | 23 + .../res/layout/item_book_group_manage.xml | 35 + .../main/res/layout/item_book_manga_edge.xml | 16 + .../main/res/layout/item_book_manga_page.xml | 64 + app/src/main/res/layout/item_book_source.xml | 132 + app/src/main/res/layout/item_bookmark.xml | 32 + .../main/res/layout/item_bookshelf_grid.xml | 99 + .../res/layout/item_bookshelf_grid_group.xml | 90 + .../main/res/layout/item_bookshelf_list.xml | 204 + .../res/layout/item_bookshelf_list_group.xml | 172 + .../main/res/layout/item_change_source.xml | 118 + app/src/main/res/layout/item_chapter_list.xml | 79 + app/src/main/res/layout/item_check_box.xml | 16 + app/src/main/res/layout/item_cover.xml | 32 + app/src/main/res/layout/item_dict_rule.xml | 48 + app/src/main/res/layout/item_download.xml | 83 + app/src/main/res/layout/item_file.xml | 24 + app/src/main/res/layout/item_file_picker.xml | 30 + app/src/main/res/layout/item_fillet_text.xml | 18 + app/src/main/res/layout/item_find_book.xml | 75 + app/src/main/res/layout/item_font.xml | 27 + app/src/main/res/layout/item_group_manage.xml | 36 + app/src/main/res/layout/item_group_select.xml | 27 + app/src/main/res/layout/item_http_tts.xml | 58 + .../main/res/layout/item_icon_preference.xml | 34 + app/src/main/res/layout/item_import_book.xml | 90 + app/src/main/res/layout/item_log.xml | 7 + app/src/main/res/layout/item_path_picker.xml | 22 + app/src/main/res/layout/item_radio_button.xml | 14 + app/src/main/res/layout/item_read_record.xml | 82 + app/src/main/res/layout/item_read_style.xml | 13 + app/src/main/res/layout/item_replace_rule.xml | 65 + app/src/main/res/layout/item_rss.xml | 41 + app/src/main/res/layout/item_rss_article.xml | 51 + .../main/res/layout/item_rss_article_1.xml | 64 + .../main/res/layout/item_rss_article_2.xml | 54 + .../main/res/layout/item_rss_read_record.xml | 36 + app/src/main/res/layout/item_rss_source.xml | 51 + app/src/main/res/layout/item_rule_sub.xml | 74 + app/src/main/res/layout/item_search.xml | 113 + app/src/main/res/layout/item_search_list.xml | 19 + .../main/res/layout/item_server_select.xml | 43 + app/src/main/res/layout/item_source_edit.xml | 16 + .../res/layout/item_source_edit_check_box.xml | 22 + .../main/res/layout/item_source_import.xml | 50 + app/src/main/res/layout/item_text.xml | 15 + app/src/main/res/layout/item_theme_config.xml | 44 + app/src/main/res/layout/item_toc_regex.xml | 67 + app/src/main/res/layout/item_txt_toc_rule.xml | 71 + app/src/main/res/layout/popup_action.xml | 21 + app/src/main/res/layout/popup_action_menu.xml | 48 + .../main/res/layout/popup_keyboard_tool.xml | 11 + app/src/main/res/layout/popup_seek_bar.xml | 21 + .../main/res/layout/view_action_button.xml | 8 + app/src/main/res/layout/view_book_page.xml | 143 + .../main/res/layout/view_detail_seek_bar.xml | 54 + app/src/main/res/layout/view_dynamic.xml | 20 + app/src/main/res/layout/view_error.xml | 27 + app/src/main/res/layout/view_fastscroller.xml | 47 + app/src/main/res/layout/view_icon.xml | 5 + app/src/main/res/layout/view_load_more.xml | 32 + app/src/main/res/layout/view_loading.xml | 19 + app/src/main/res/layout/view_manga_menu.xml | 132 + .../main/res/layout/view_navigation_badge.xml | 16 + app/src/main/res/layout/view_preference.xml | 66 + .../res/layout/view_preference_category.xml | 31 + app/src/main/res/layout/view_read_menu.xml | 468 ++ .../main/res/layout/view_refresh_recycler.xml | 16 + app/src/main/res/layout/view_search.xml | 19 + app/src/main/res/layout/view_search_menu.xml | 288 ++ .../res/layout/view_select_action_bar.xml | 60 + app/src/main/res/layout/view_tab_layout.xml | 10 + .../main/res/layout/view_tab_layout_min.xml | 12 + app/src/main/res/layout/view_title_bar.xml | 9 + .../main/res/layout/view_title_bar_dark.xml | 9 + app/src/main/res/layout/view_toast.xml | 27 + app/src/main/res/menu/about.xml | 18 + app/src/main/res/menu/app_log.xml | 12 + app/src/main/res/menu/app_update.xml | 10 + app/src/main/res/menu/audio_play.xml | 40 + app/src/main/res/menu/backup_restore.xml | 18 + app/src/main/res/menu/book_cache.xml | 90 + app/src/main/res/menu/book_cache_download.xml | 17 + app/src/main/res/menu/book_group_manage.xml | 13 + app/src/main/res/menu/book_info.xml | 85 + app/src/main/res/menu/book_info_edit.xml | 11 + app/src/main/res/menu/book_manga.xml | 126 + app/src/main/res/menu/book_read.xml | 155 + .../main/res/menu/book_read_change_source.xml | 17 + app/src/main/res/menu/book_read_record.xml | 45 + app/src/main/res/menu/book_read_refresh.xml | 23 + app/src/main/res/menu/book_read_source.xml | 27 + app/src/main/res/menu/book_remote.xml | 56 + app/src/main/res/menu/book_search.xml | 28 + app/src/main/res/menu/book_search_scope.xml | 15 + app/src/main/res/menu/book_source.xml | 147 + app/src/main/res/menu/book_source_debug.xml | 43 + app/src/main/res/menu/book_source_item.xml | 32 + app/src/main/res/menu/book_source_sel.xml | 66 + app/src/main/res/menu/book_toc.xml | 74 + app/src/main/res/menu/bookmark.xml | 12 + app/src/main/res/menu/bookshelf_manage.xml | 32 + .../main/res/menu/bookshelf_menage_sel.xml | 32 + app/src/main/res/menu/change_cover.xml | 13 + app/src/main/res/menu/change_source.xml | 72 + app/src/main/res/menu/change_source_item.xml | 24 + app/src/main/res/menu/code_edit.xml | 13 + app/src/main/res/menu/content_edit.xml | 23 + app/src/main/res/menu/content_search.xml | 13 + .../main/res/menu/content_select_action.xml | 39 + app/src/main/res/menu/crash_log.xml | 12 + app/src/main/res/menu/dialog_text.xml | 9 + app/src/main/res/menu/dict_rule.xml | 43 + app/src/main/res/menu/dict_rule_edit.xml | 23 + app/src/main/res/menu/dict_rule_sel.xml | 20 + .../res/menu/direct_link_upload_config.xml | 20 + app/src/main/res/menu/explore_item.xml | 28 + app/src/main/res/menu/file_chooser.xml | 11 + app/src/main/res/menu/file_long_click.xml | 8 + app/src/main/res/menu/font_select.xml | 15 + app/src/main/res/menu/group_manage.xml | 10 + app/src/main/res/menu/import_book.xml | 53 + app/src/main/res/menu/import_book_sel.xml | 8 + app/src/main/res/menu/import_replace.xml | 12 + app/src/main/res/menu/import_source.xml | 40 + .../main/res/menu/keyboard_assists_config.xml | 11 + app/src/main/res/menu/main_bnv.xml | 23 + app/src/main/res/menu/main_bookshelf.xml | 79 + app/src/main/res/menu/main_explore.xml | 15 + app/src/main/res/menu/main_my.xml | 13 + app/src/main/res/menu/main_rss.xml | 29 + app/src/main/res/menu/open_url_confirm.xml | 17 + app/src/main/res/menu/qr_code_scan.xml | 10 + app/src/main/res/menu/replace_edit.xml | 23 + app/src/main/res/menu/replace_rule.xml | 61 + app/src/main/res/menu/replace_rule_item.xml | 16 + app/src/main/res/menu/replace_rule_sel.xml | 30 + app/src/main/res/menu/rss_articles.xml | 40 + app/src/main/res/menu/rss_favorites.xml | 26 + app/src/main/res/menu/rss_main_item.xml | 20 + app/src/main/res/menu/rss_read.xml | 38 + app/src/main/res/menu/rss_read_record.xml | 12 + app/src/main/res/menu/rss_source.xml | 79 + app/src/main/res/menu/rss_source_debug.xml | 17 + app/src/main/res/menu/rss_source_item.xml | 16 + app/src/main/res/menu/rss_source_sel.xml | 50 + app/src/main/res/menu/save.xml | 13 + app/src/main/res/menu/search_view.xml | 11 + app/src/main/res/menu/server_config.xml | 11 + app/src/main/res/menu/servers.xml | 11 + app/src/main/res/menu/source_edit.xml | 75 + app/src/main/res/menu/source_login.xml | 28 + app/src/main/res/menu/source_picker.xml | 8 + app/src/main/res/menu/source_sub_item.xml | 8 + app/src/main/res/menu/source_subscription.xml | 11 + .../main/res/menu/source_webview_login.xml | 13 + app/src/main/res/menu/speak_engine.xml | 33 + app/src/main/res/menu/speak_engine_edit.xml | 48 + app/src/main/res/menu/theme_config.xml | 10 + app/src/main/res/menu/theme_list.xml | 10 + app/src/main/res/menu/txt_toc_rule.xml | 43 + app/src/main/res/menu/txt_toc_rule_edit.xml | 19 + app/src/main/res/menu/txt_toc_rule_item.xml | 16 + app/src/main/res/menu/txt_toc_rule_sel.xml | 20 + app/src/main/res/menu/verification_code.xml | 23 + app/src/main/res/menu/web_view.xml | 38 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../main/res/mipmap-anydpi-v26/launcher1.xml | 6 + .../main/res/mipmap-anydpi-v26/launcher2.xml | 6 + .../main/res/mipmap-anydpi-v26/launcher3.xml | 6 + .../main/res/mipmap-anydpi-v26/launcher4.xml | 6 + .../main/res/mipmap-anydpi-v26/launcher5.xml | 6 + .../main/res/mipmap-anydpi-v26/launcher6.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2364 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1599 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3246 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5136 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7656 bytes app/src/main/res/raw/silent_sound.mp3 | Bin 0 -> 2160 bytes app/src/main/res/values-es-rES/arrays.xml | 121 + app/src/main/res/values-es-rES/strings.xml | 1229 +++++ app/src/main/res/values-ja-rJP/strings.xml | 1232 +++++ app/src/main/res/values-night/colors.xml | 42 + app/src/main/res/values-night/styles.xml | 21 + app/src/main/res/values-pt-rBR/arrays.xml | 121 + app/src/main/res/values-pt-rBR/strings.xml | 1232 +++++ app/src/main/res/values-vi/arrays.xml | 99 + app/src/main/res/values-vi/strings.xml | 1223 +++++ app/src/main/res/values-zh-rHK/arrays.xml | 84 + app/src/main/res/values-zh-rHK/strings.xml | 1227 +++++ app/src/main/res/values-zh-rTW/arrays.xml | 119 + app/src/main/res/values-zh-rTW/strings.xml | 1228 +++++ app/src/main/res/values-zh/arrays.xml | 117 + app/src/main/res/values-zh/strings.xml | 1234 +++++ app/src/main/res/values/array_values.xml | 56 + app/src/main/res/values/arrays.xml | 188 + app/src/main/res/values/attrs.xml | 238 + app/src/main/res/values/colors.xml | 116 + .../res/values/colors_material_design.xml | 338 ++ app/src/main/res/values/dimens.xml | 39 + app/src/main/res/values/ids.xml | 17 + app/src/main/res/values/non_translat.xml | 17 + app/src/main/res/values/strings.xml | 1236 +++++ app/src/main/res/values/styles.xml | 130 + app/src/main/res/xml/about.xml | 64 + app/src/main/res/xml/file_paths.xml | 24 + .../main/res/xml/network_security_config.xml | 13 + app/src/main/res/xml/pref_config_aloud.xml | 72 + app/src/main/res/xml/pref_config_backup.xml | 119 + app/src/main/res/xml/pref_config_cover.xml | 98 + app/src/main/res/xml/pref_config_other.xml | 229 + app/src/main/res/xml/pref_config_read.xml | 202 + app/src/main/res/xml/pref_config_theme.xml | 172 + app/src/main/res/xml/pref_config_welcome.xml | 84 + app/src/main/res/xml/pref_main.xml | 127 + app/src/main/res/xml/spen_remote_actions.xml | 23 + .../java/io/legado/app/ExampleUnitTest.kt | 19 + app/src/test/java/io/legado/app/JsTest.kt | 135 + avd.bat | 1 + avd.sh | 1 + build.gradle | 37 + gradle.properties | 48 + gradle/libs.versions.toml | 218 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + modules/book/.gitignore | 1 + modules/book/build.gradle | 34 + modules/book/consumer-rules.pro | 0 modules/book/src/main/AndroidManifest.xml | 4 + .../src/main/java/me/ag2s/base/PfdHelper.java | 102 + .../java/me/ag2s/base/ThrowableUtils.java | 15 + .../main/java/me/ag2s/epublib/Constants.java | 13 + .../browsersupport/NavigationEvent.java | 157 + .../NavigationEventListener.java | 17 + .../browsersupport/NavigationHistory.java | 207 + .../epublib/browsersupport/Navigator.java | 219 + .../epublib/browsersupport/package-info.java | 7 + .../java/me/ag2s/epublib/domain/Author.java | 88 + .../java/me/ag2s/epublib/domain/Date.java | 112 + .../java/me/ag2s/epublib/domain/EpubBook.java | 323 ++ .../epublib/domain/EpubResourceProvider.java | 37 + .../epublib/domain/FileResourceProvider.java | 46 + .../java/me/ag2s/epublib/domain/Guide.java | 128 + .../ag2s/epublib/domain/GuideReference.java | 100 + .../me/ag2s/epublib/domain/Identifier.java | 133 + .../me/ag2s/epublib/domain/LazyResource.java | 145 + .../epublib/domain/LazyResourceProvider.java | 12 + .../domain/ManifestItemProperties.java | 22 + .../domain/ManifestItemRefProperties.java | 17 + .../epublib/domain/ManifestProperties.java | 6 + .../me/ag2s/epublib/domain/MediaType.java | 73 + .../me/ag2s/epublib/domain/MediaTypes.java | 102 + .../java/me/ag2s/epublib/domain/Metadata.java | 241 + .../java/me/ag2s/epublib/domain/Relator.java | 1143 +++++ .../java/me/ag2s/epublib/domain/Resource.java | 336 ++ .../epublib/domain/ResourceInputStream.java | 33 + .../epublib/domain/ResourceReference.java | 43 + .../me/ag2s/epublib/domain/Resources.java | 418 ++ .../java/me/ag2s/epublib/domain/Spine.java | 190 + .../ag2s/epublib/domain/SpineReference.java | 52 + .../me/ag2s/epublib/domain/TOCReference.java | 57 + .../ag2s/epublib/domain/TableOfContents.java | 264 ++ .../domain/TitledResourceReference.java | 91 + .../me/ag2s/epublib/epub/BookProcessor.java | 20 + .../epublib/epub/BookProcessorPipeline.java | 73 + .../java/me/ag2s/epublib/epub/DOMUtil.java | 178 + .../epublib/epub/EpubProcessorSupport.java | 139 + .../java/me/ag2s/epublib/epub/EpubReader.java | 186 + .../java/me/ag2s/epublib/epub/EpubWriter.java | 206 + .../epublib/epub/EpubWriterProcessor.java | 57 + .../me/ag2s/epublib/epub/HtmlProcessor.java | 11 + .../me/ag2s/epublib/epub/NCXDocumentV2.java | 344 ++ .../me/ag2s/epublib/epub/NCXDocumentV3.java | 461 ++ .../epublib/epub/PackageDocumentBase.java | 96 + .../epub/PackageDocumentMetadataReader.java | 225 + .../epub/PackageDocumentMetadataWriter.java | 187 + .../epublib/epub/PackageDocumentReader.java | 508 +++ .../epublib/epub/PackageDocumentWriter.java | 247 + .../me/ag2s/epublib/epub/ResourcesLoader.java | 177 + .../me/ag2s/epublib/util/CollectionUtil.java | 72 + .../java/me/ag2s/epublib/util/IOUtil.java | 959 ++++ .../epublib/util/NoCloseOutputStream.java | 33 + .../me/ag2s/epublib/util/NoCloseWriter.java | 36 + .../me/ag2s/epublib/util/ResourceUtil.java | 181 + .../java/me/ag2s/epublib/util/StringUtil.java | 305 ++ .../me/ag2s/epublib/util/URLEncodeUtil.java | 118 + .../util/commons/io/BOMInputStream.java | 383 ++ .../util/commons/io/ByteOrderMark.java | 203 + .../epublib/util/commons/io/IOConsumer.java | 59 + .../util/commons/io/ProxyInputStream.java | 252 ++ .../util/commons/io/XmlStreamReader.java | 819 ++++ .../commons/io/XmlStreamReaderException.java | 139 + .../epublib/util/zip/AndroidZipEntry.java | 363 ++ .../ag2s/epublib/util/zip/AndroidZipFile.java | 490 ++ .../ag2s/epublib/util/zip/ZipConstants.java | 61 + .../epublib/util/zip/ZipEntryWrapper.java | 75 + .../ag2s/epublib/util/zip/ZipException.java | 34 + .../ag2s/epublib/util/zip/ZipFileWrapper.java | 103 + .../java/me/ag2s/umdlib/domain/UmdBook.java | 88 + .../me/ag2s/umdlib/domain/UmdChapters.java | 212 + .../java/me/ag2s/umdlib/domain/UmdCover.java | 97 + .../java/me/ag2s/umdlib/domain/UmdEnd.java | 20 + .../java/me/ag2s/umdlib/domain/UmdHeader.java | 164 + .../me/ag2s/umdlib/tool/StreamReader.java | 134 + .../java/me/ag2s/umdlib/tool/UmdUtils.java | 129 + .../me/ag2s/umdlib/tool/WrapOutputStream.java | 91 + .../java/me/ag2s/umdlib/umd/UmdReader.java | 223 + .../dtd/openebook.org/dtds/oeb-1.2/oeb12.ent | 1135 +++++ .../openebook.org/dtds/oeb-1.2/oebpkg12.dtd | 277 ++ .../www.daisy.org/z3986/2005/ncx-2005-1.dtd | 187 + .../dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod | 142 + .../xhtml-modularization/DTD/xhtml-arch-1.mod | 36 + .../DTD/xhtml-attribs-1.mod | 96 + .../xhtml-modularization/DTD/xhtml-base-1.mod | 40 + .../xhtml-modularization/DTD/xhtml-bdo-1.mod | 33 + .../DTD/xhtml-blkphras-1.mod | 74 + .../DTD/xhtml-blkpres-1.mod | 30 + .../DTD/xhtml-blkstruct-1.mod | 34 + .../DTD/xhtml-charent-1.mod | 30 + .../DTD/xhtml-csismap-1.mod | 69 + .../DTD/xhtml-datatypes-1.mod | 97 + .../DTD/xhtml-datatypes-1.mod.1 | 103 + .../xhtml-modularization/DTD/xhtml-edit-1.mod | 38 + .../DTD/xhtml-events-1.mod | 97 + .../xhtml-modularization/DTD/xhtml-form-1.mod | 130 + .../DTD/xhtml-framework-1.mod | 58 + .../DTD/xhtml-hypertext-1.mod | 34 + .../DTD/xhtml-image-1.mod | 35 + .../DTD/xhtml-inlphras-1.mod | 70 + .../DTD/xhtml-inlpres-1.mod | 54 + .../DTD/xhtml-inlstruct-1.mod | 42 + .../DTD/xhtml-inlstyle-1.mod | 30 + .../xhtml-modularization/DTD/xhtml-lat1.ent | 196 + .../xhtml-modularization/DTD/xhtml-link-1.mod | 42 + .../xhtml-modularization/DTD/xhtml-list-1.mod | 64 + .../xhtml-modularization/DTD/xhtml-meta-1.mod | 32 + .../DTD/xhtml-notations-1.mod | 94 + .../DTD/xhtml-object-1.mod | 36 + .../DTD/xhtml-param-1.mod | 33 + .../xhtml-modularization/DTD/xhtml-pres-1.mod | 28 + .../DTD/xhtml-qname-1.mod | 224 + .../DTD/xhtml-script-1.mod | 39 + .../DTD/xhtml-special.ent | 80 + .../DTD/xhtml-ssismap-1.mod | 28 + .../DTD/xhtml-struct-1.mod | 75 + .../DTD/xhtml-style-1.mod | 32 + .../xhtml-modularization/DTD/xhtml-symbol.ent | 237 + .../DTD/xhtml-symbol.ent.1 | 237 + .../DTD/xhtml-table-1.mod | 177 + .../xhtml-modularization/DTD/xhtml-text-1.mod | 32 + .../DTD/xhtml11-model-1.mod | 222 + .../www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent | 196 + .../TR/xhtml1/DTD/xhtml-special.ent | 80 + .../www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent | 237 + .../TR/xhtml1/DTD/xhtml1-strict.dtd | 580 +++ .../TR/xhtml1/DTD/xhtml1-transitional.dtd | 685 +++ .../dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd | 167 + .../book/src/main/resources/log4j.properties | 52 + modules/rhino/build.gradle | 47 + modules/rhino/consumer-rules.pro | 54 + modules/rhino/lib/rhino-1.7.14.jar | Bin 0 -> 1442544 bytes modules/rhino/src/main/AndroidManifest.xml | 4 + .../java/com/script/AbstractScriptEngine.kt | 116 + .../src/main/java/com/script/Bindings.kt | 18 + .../src/main/java/com/script/Compilable.kt | 15 + .../main/java/com/script/CompiledScript.kt | 50 + .../src/main/java/com/script/Invocable.kt | 16 + .../java/com/script/RhinoContextFactory.kt | 7 + .../main/java/com/script/ScriptBindings.kt | 46 + .../com/script/ScriptBindingsExtensions.kt | 14 + .../src/main/java/com/script/ScriptContext.kt | 37 + .../src/main/java/com/script/ScriptEngine.kt | 71 + .../main/java/com/script/ScriptException.kt | 54 + .../main/java/com/script/SimpleBindings.kt | 54 + .../java/com/script/SimpleScriptContext.kt | 109 + .../java/com/script/rhino/ClassNameMatcher.kt | 41 + .../com/script/rhino/CollectionExtensions.kt | 22 + .../com/script/rhino/ExternalScriptable.kt | 294 ++ .../com/script/rhino/InterfaceImplementor.kt | 95 + .../main/java/com/script/rhino/JSAdapter.kt | 307 ++ .../main/java/com/script/rhino/JavaAdapter.kt | 104 + .../com/script/rhino/JavaObjectWrapFactory.kt | 9 + .../script/rhino/ProtectedNativeJavaClass.kt | 44 + .../com/script/rhino/ReadOnlyJavaObject.kt | 43 + .../com/script/rhino/RhinoClassShutter.kt | 190 + .../com/script/rhino/RhinoCompiledScript.kt | 135 + .../java/com/script/rhino/RhinoContext.kt | 31 + .../main/java/com/script/rhino/RhinoErrors.kt | 5 + .../java/com/script/rhino/RhinoExtensions.kt | 65 + .../com/script/rhino/RhinoScriptEngine.kt | 446 ++ .../java/com/script/rhino/RhinoTopLevel.kt | 111 + .../java/com/script/rhino/RhinoWrapFactory.kt | 91 + .../java/com/script/rhino/VMBridgeReflect.kt | 20 + modules/web/.browserslistrc | 3 + modules/web/.editorconfig | 6 + modules/web/.gitignore | 30 + modules/web/.prettierignore | 3 + modules/web/.prettierrc.json | 7 + modules/web/LICENSE | 674 +++ modules/web/README.md | 19 + modules/web/env.d.ts | 3 + modules/web/eslint.config.mjs | 19 + modules/web/index.html | 13 + modules/web/package.json | 51 + modules/web/public/favicon.ico | Bin 0 -> 4286 bytes modules/web/scripts/sync.js | 45 + modules/web/src/App.vue | 3 + modules/web/src/api/api.ts | 226 + modules/web/src/api/axios.ts | 15 + modules/web/src/api/index.ts | 113 + modules/web/src/assets/bookshelf.css | 14 + modules/web/src/assets/code.css | 9 + modules/web/src/assets/fonts/iconfont.css | 5 + modules/web/src/assets/fonts/iconfont.woff | Bin 0 -> 30972 bytes modules/web/src/assets/fonts/popfont.css | 7 + modules/web/src/assets/fonts/popfont.ttf | Bin 0 -> 4184 bytes modules/web/src/assets/fonts/shelffont.css | 7 + modules/web/src/assets/fonts/shelffont.ttf | Bin 0 -> 10072 bytes modules/web/src/assets/imgs/github.png | Bin 0 -> 1090 bytes modules/web/src/assets/imgs/themes/body_0.png | Bin 0 -> 749 bytes modules/web/src/assets/imgs/themes/body_1.png | Bin 0 -> 1586 bytes modules/web/src/assets/imgs/themes/body_2.png | Bin 0 -> 677 bytes modules/web/src/assets/imgs/themes/body_3.png | Bin 0 -> 710 bytes modules/web/src/assets/imgs/themes/body_5.png | Bin 0 -> 709 bytes modules/web/src/assets/imgs/themes/body_6.png | Bin 0 -> 744 bytes .../web/src/assets/imgs/themes/content_0.png | Bin 0 -> 559 bytes .../web/src/assets/imgs/themes/content_1.png | Bin 0 -> 1279 bytes .../web/src/assets/imgs/themes/content_2.png | Bin 0 -> 653 bytes .../web/src/assets/imgs/themes/content_3.png | Bin 0 -> 693 bytes .../web/src/assets/imgs/themes/content_5.png | Bin 0 -> 755 bytes .../web/src/assets/imgs/themes/content_6.png | Bin 0 -> 565 bytes .../web/src/assets/imgs/themes/popup_0.png | Bin 0 -> 713 bytes .../web/src/assets/imgs/themes/popup_1.png | Bin 0 -> 617 bytes .../web/src/assets/imgs/themes/popup_2.png | Bin 0 -> 825 bytes .../web/src/assets/imgs/themes/popup_3.png | Bin 0 -> 653 bytes .../web/src/assets/imgs/themes/popup_5.png | Bin 0 -> 770 bytes .../web/src/assets/imgs/themes/popup_6.png | Bin 0 -> 565 bytes modules/web/src/assets/kbd.css | 12 + modules/web/src/assets/sourceeditor.css | 13 + modules/web/src/auto-imports.d.ts | 94 + modules/web/src/book.d.ts | 109 + modules/web/src/components.d.ts | 44 + modules/web/src/components/BookItems.vue | 196 + modules/web/src/components/CatalogItem.vue | 51 + modules/web/src/components/ChapterContent.vue | 190 + modules/web/src/components/PopCatalog.vue | 136 + modules/web/src/components/ReadSettings.vue | 596 +++ modules/web/src/components/SourceDebug.vue | 67 + modules/web/src/components/SourceHelp.vue | 63 + modules/web/src/components/SourceItem.vue | 56 + modules/web/src/components/SourceJson.vue | 44 + modules/web/src/components/SourceList.vue | 161 + modules/web/src/components/SourceTabForm.vue | 89 + modules/web/src/components/SourceTabTools.vue | 39 + modules/web/src/components/ToolBar.vue | 350 ++ .../web/src/config/bookSourceEditConfig.ts | 585 +++ modules/web/src/config/rssSourceEditConfig.ts | 222 + modules/web/src/config/sourceConfig.d.ts | 18 + modules/web/src/config/themeConfig.ts | 65 + modules/web/src/hooks/loading.css | 8 + modules/web/src/hooks/loading.ts | 40 + modules/web/src/main.ts | 22 + modules/web/src/pages/bookshelf/README.md | 33 + modules/web/src/pages/bookshelf/index.html | 13 + modules/web/src/pages/bookshelf/main.js | 19 + modules/web/src/pages/source/README.md | 34 + modules/web/src/pages/source/index.html | 13 + modules/web/src/pages/source/main.js | 7 + modules/web/src/plugins/jump.d.ts | 18 + modules/web/src/plugins/jump.js | 186 + modules/web/src/router/bookRouter.ts | 22 + modules/web/src/router/index.ts | 15 + modules/web/src/router/sourceRouter.ts | 23 + modules/web/src/source.d.ts | 165 + modules/web/src/store/bookStore.ts | 210 + modules/web/src/store/connectionStore.ts | 24 + modules/web/src/store/index.ts | 6 + modules/web/src/store/sourceStore.ts | 134 + modules/web/src/utils/souce.ts | 75 + modules/web/src/utils/utils.ts | 55 + modules/web/src/views/BookChapter.vue | 770 ++++ modules/web/src/views/BookShelf.vue | 488 ++ modules/web/src/views/SourceEditor.vue | 42 + modules/web/src/web.d.ts | 14 + modules/web/tsconfig.app.json | 16 + modules/web/tsconfig.json | 11 + modules/web/tsconfig.node.json | 19 + modules/web/vite.config.ts | 76 + package.json | 28 + settings.gradle | 52 + 1815 files changed, 315166 insertions(+) create mode 100755 .github/ISSUE_TEMPLATE/01-bugReport.yml create mode 100755 .github/ISSUE_TEMPLATE/02-featureRequest.yml create mode 100755 .github/ISSUE_TEMPLATE/config.yml create mode 100755 .github/dependabot.yml create mode 100755 .github/scripts/cronet.sh create mode 100755 .github/scripts/lzy_web.py create mode 100755 .github/scripts/tg_bot.py create mode 100755 .github/workflows/autoupdatefork.yml create mode 100755 .github/workflows/cronet.yml create mode 100755 .github/workflows/legado.jks create mode 100755 .github/workflows/release.yml create mode 100755 .github/workflows/stale.yml create mode 100755 .github/workflows/test.yml create mode 100755 .github/workflows/web.yml create mode 100755 .gitignore create mode 100755 CHANGELOG.md create mode 100755 English.md create mode 100755 LICENSE create mode 100755 api.md create mode 100755 app/.gitignore create mode 100755 app/build.gradle create mode 100755 app/cronet-proguard-rules.pro create mode 100755 app/cronetlib/cronet_api.jar create mode 100755 app/cronetlib/cronet_impl_common_java.jar create mode 100755 app/cronetlib/cronet_impl_native_java.jar create mode 100755 app/cronetlib/cronet_impl_platform_java.jar create mode 100755 app/cronetlib/cronet_shared_java.jar create mode 100755 app/download.gradle create mode 100755 app/google-services.json create mode 100755 app/proguard-rules.pro create mode 100755 app/schemas/io.legado.app.data.AppDatabase/1.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/10.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/11.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/12.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/13.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/14.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/15.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/16.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/17.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/18.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/19.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/2.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/20.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/21.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/22.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/23.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/24.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/25.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/26.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/27.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/28.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/29.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/3.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/30.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/31.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/32.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/33.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/34.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/35.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/36.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/37.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/38.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/39.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/4.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/40.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/41.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/42.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/43.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/44.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/45.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/46.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/47.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/48.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/49.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/5.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/50.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/51.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/52.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/53.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/54.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/55.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/56.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/57.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/58.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/59.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/6.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/60.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/61.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/62.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/63.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/64.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/65.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/66.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/67.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/68.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/69.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/7.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/70.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/71.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/72.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/73.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/74.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/75.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/8.json create mode 100755 app/schemas/io.legado.app.data.AppDatabase/9.json create mode 100755 app/src/androidTest/java/io/legado/app/AndroidJsTest.kt create mode 100755 app/src/androidTest/java/io/legado/app/ExampleInstrumentedTest.kt create mode 100755 app/src/androidTest/java/io/legado/app/HttpTest.kt create mode 100755 app/src/androidTest/java/io/legado/app/HttpTtsTest.kt create mode 100755 app/src/androidTest/java/io/legado/app/MigrationTest.kt create mode 100755 app/src/androidTest/java/io/legado/app/UpdateTest.kt create mode 100755 app/src/debug/res/values-zh/strings.xml create mode 100755 app/src/debug/res/values/strings.xml create mode 100755 app/src/main/AndroidManifest.xml create mode 100755 app/src/main/assets/18PlusList.txt create mode 100755 app/src/main/assets/LICENSE.md create mode 100755 app/src/main/assets/bg/午后沙滩.jpg create mode 100755 app/src/main/assets/bg/宁静夜色.jpg create mode 100755 app/src/main/assets/bg/山水墨影.jpg create mode 100755 app/src/main/assets/bg/山水画.jpg create mode 100755 app/src/main/assets/bg/护眼漫绿.jpg create mode 100755 app/src/main/assets/bg/新羊皮纸.jpg create mode 100755 app/src/main/assets/bg/明媚倾城.jpg create mode 100755 app/src/main/assets/bg/深宫魅影.jpg create mode 100755 app/src/main/assets/bg/清新时光.jpg create mode 100755 app/src/main/assets/bg/羊皮纸1.jpg create mode 100755 app/src/main/assets/bg/羊皮纸2.jpg create mode 100755 app/src/main/assets/bg/羊皮纸3.jpg create mode 100755 app/src/main/assets/bg/羊皮纸4.jpg create mode 100755 app/src/main/assets/bg/边彩画布.jpg create mode 100755 app/src/main/assets/cronet.json create mode 100755 app/src/main/assets/defaultData/bookSources.json create mode 100755 app/src/main/assets/defaultData/coverRule.json create mode 100755 app/src/main/assets/defaultData/dictRules.json create mode 100755 app/src/main/assets/defaultData/directLinkUpload.json create mode 100755 app/src/main/assets/defaultData/httpTTS.json create mode 100755 app/src/main/assets/defaultData/keyboardAssists.json create mode 100755 app/src/main/assets/defaultData/readConfig.json create mode 100755 app/src/main/assets/defaultData/rssSources.json create mode 100755 app/src/main/assets/defaultData/themeConfig.json create mode 100755 app/src/main/assets/defaultData/txtTocRule.json create mode 100755 app/src/main/assets/disclaimer.md create mode 100755 app/src/main/assets/epub/chapter.html create mode 100755 app/src/main/assets/epub/cover.html create mode 100755 app/src/main/assets/epub/fonts.css create mode 100755 app/src/main/assets/epub/intro.html create mode 100755 app/src/main/assets/epub/logo.png create mode 100755 app/src/main/assets/epub/main.css create mode 100755 app/src/main/assets/font/number.ttf create mode 100755 app/src/main/assets/privacyPolicy.md create mode 100755 app/src/main/assets/storageHelp.md create mode 100755 app/src/main/assets/updateLog.md create mode 100755 app/src/main/assets/web/assets/css/main.css create mode 100755 app/src/main/assets/web/assets/js/dist.js create mode 100755 app/src/main/assets/web/assets/js/md5.js create mode 100755 app/src/main/assets/web/favicon.ico create mode 100755 app/src/main/assets/web/help/css/github-markdown-light.min.css create mode 100755 app/src/main/assets/web/help/css/highlight.min.css create mode 100755 app/src/main/assets/web/help/index.html create mode 100755 app/src/main/assets/web/help/js/highlight.min.js create mode 100755 app/src/main/assets/web/help/js/main.js create mode 100755 app/src/main/assets/web/help/js/marked-highlight.umd.js create mode 100755 app/src/main/assets/web/help/js/marked.min.js create mode 100755 app/src/main/assets/web/help/js/require.js create mode 100755 app/src/main/assets/web/help/md/ExtensionContentType.md create mode 100755 app/src/main/assets/web/help/md/SourceMBookHelp.md create mode 100755 app/src/main/assets/web/help/md/SourceMRssHelp.md create mode 100755 app/src/main/assets/web/help/md/appHelp.md create mode 100755 app/src/main/assets/web/help/md/debugHelp.md create mode 100755 app/src/main/assets/web/help/md/dictRuleHelp.md create mode 100755 app/src/main/assets/web/help/md/httpTTSHelp.md create mode 100755 app/src/main/assets/web/help/md/jsHelp.md create mode 100755 app/src/main/assets/web/help/md/readMenuHelp.md create mode 100755 app/src/main/assets/web/help/md/regexHelp.md create mode 100755 app/src/main/assets/web/help/md/replaceRuleHelp.md create mode 100755 app/src/main/assets/web/help/md/ruleHelp.md create mode 100755 app/src/main/assets/web/help/md/txtTocRuleHelp.md create mode 100755 app/src/main/assets/web/help/md/webDavBookHelp.md create mode 100755 app/src/main/assets/web/help/md/webDavHelp.md create mode 100755 app/src/main/assets/web/help/md/xpathHelp.md create mode 100755 app/src/main/assets/web/images/bg.jpg create mode 100755 app/src/main/assets/web/index.html create mode 100755 app/src/main/assets/web/uploadBook/css/wifi_send.css create mode 100755 app/src/main/assets/web/uploadBook/img/close.png create mode 100755 app/src/main/assets/web/uploadBook/img/logo.png create mode 100755 app/src/main/assets/web/uploadBook/img/notice01.png create mode 100755 app/src/main/assets/web/uploadBook/img/notice02.png create mode 100755 app/src/main/assets/web/uploadBook/img/right.png create mode 100755 app/src/main/assets/web/uploadBook/img/safari.png create mode 100755 app/src/main/assets/web/uploadBook/img/wrong.png create mode 100755 app/src/main/assets/web/uploadBook/index.html create mode 100755 app/src/main/assets/web/uploadBook/js/common.js create mode 100755 app/src/main/assets/web/uploadBook/js/html5_fun.js create mode 100755 app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js create mode 100755 app/src/main/assets/web/vue/assets/BookChapter-BsiFtdIw.css create mode 100755 app/src/main/assets/web/vue/assets/BookChapter-Cs3stH93.js create mode 100755 app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css create mode 100755 app/src/main/assets/web/vue/assets/BookShelf-DIQtBULC.js create mode 100755 app/src/main/assets/web/vue/assets/iconfont-PstzbNMW.woff create mode 100755 app/src/main/assets/web/vue/assets/index-CrxHVQK7.css create mode 100755 app/src/main/assets/web/vue/assets/index-Wr40-hHf.js create mode 100755 app/src/main/assets/web/vue/assets/loading-C4J6hIxs.js create mode 100755 app/src/main/assets/web/vue/assets/loading-DkQYEuap.css create mode 100755 app/src/main/assets/web/vue/assets/popfont-WaOB0hHG.ttf create mode 100755 app/src/main/assets/web/vue/assets/shelffont-D-W4UqG-.ttf create mode 100755 app/src/main/assets/web/vue/assets/vendor-CXe1BRiH.css create mode 100755 app/src/main/assets/web/vue/assets/vendor-KSDcS24u.js create mode 100755 app/src/main/assets/web/vue/favicon.ico create mode 100755 app/src/main/assets/web/vue/index.html create mode 100755 app/src/main/ic_launcher-web.png create mode 100755 app/src/main/java/io/legado/app/App.kt create mode 100755 app/src/main/java/io/legado/app/README.md create mode 100755 app/src/main/java/io/legado/app/api/ReaderProvider.kt create mode 100755 app/src/main/java/io/legado/app/api/ReturnData.kt create mode 100755 app/src/main/java/io/legado/app/api/ShortCuts.kt create mode 100755 app/src/main/java/io/legado/app/api/controller/BookController.kt create mode 100755 app/src/main/java/io/legado/app/api/controller/BookSourceController.kt create mode 100755 app/src/main/java/io/legado/app/api/controller/ReplaceRuleController.kt create mode 100755 app/src/main/java/io/legado/app/api/controller/RssSourceController.kt create mode 100755 app/src/main/java/io/legado/app/base/AppContextWrapper.kt create mode 100755 app/src/main/java/io/legado/app/base/BaseActivity.kt create mode 100755 app/src/main/java/io/legado/app/base/BaseDialogFragment.kt create mode 100755 app/src/main/java/io/legado/app/base/BaseFragment.kt create mode 100755 app/src/main/java/io/legado/app/base/BasePrefDialogFragment.kt create mode 100755 app/src/main/java/io/legado/app/base/BaseService.kt create mode 100755 app/src/main/java/io/legado/app/base/BaseViewModel.kt create mode 100755 app/src/main/java/io/legado/app/base/README.md create mode 100755 app/src/main/java/io/legado/app/base/VMBaseActivity.kt create mode 100755 app/src/main/java/io/legado/app/base/VMBaseFragment.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt create mode 100755 app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt create mode 100755 app/src/main/java/io/legado/app/constant/AppConst.kt create mode 100755 app/src/main/java/io/legado/app/constant/AppLog.kt create mode 100755 app/src/main/java/io/legado/app/constant/AppPattern.kt create mode 100755 app/src/main/java/io/legado/app/constant/BookSourceType.kt create mode 100755 app/src/main/java/io/legado/app/constant/BookType.kt create mode 100755 app/src/main/java/io/legado/app/constant/EventBus.kt create mode 100755 app/src/main/java/io/legado/app/constant/IntentAction.kt create mode 100755 app/src/main/java/io/legado/app/constant/NotificationId.kt create mode 100755 app/src/main/java/io/legado/app/constant/PageAnim.kt create mode 100755 app/src/main/java/io/legado/app/constant/PreferKey.kt create mode 100755 app/src/main/java/io/legado/app/constant/SourceType.kt create mode 100755 app/src/main/java/io/legado/app/constant/Status.kt create mode 100755 app/src/main/java/io/legado/app/constant/Theme.kt create mode 100755 app/src/main/java/io/legado/app/data/AppDatabase.kt create mode 100755 app/src/main/java/io/legado/app/data/DatabaseMigrations.kt create mode 100755 app/src/main/java/io/legado/app/data/README.md create mode 100755 app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/BookDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/CacheDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/CookieDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/DictRuleDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/KeyboardAssistsDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/RssReadRecordDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/RssStarDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/ServerDao.kt create mode 100755 app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BaseBook.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BaseRssArticle.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BaseSource.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/Book.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BookChapter.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BookGroup.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BookProgress.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BookSource.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/Bookmark.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/Cache.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/Cookie.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/DictRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/HttpTTS.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/KeyboardAssist.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/ReadRecord.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/RssArticle.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/RssSource.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/RssStar.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/RuleSub.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/SearchBook.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/Server.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/BookListRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/ExploreKind.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/FlexChildStyle.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt create mode 100755 app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt create mode 100755 app/src/main/java/io/legado/app/exception/ConcurrentException.kt create mode 100755 app/src/main/java/io/legado/app/exception/ContentEmptyException.kt create mode 100755 app/src/main/java/io/legado/app/exception/EmptyFileException.kt create mode 100755 app/src/main/java/io/legado/app/exception/InvalidBooksDirException.kt create mode 100755 app/src/main/java/io/legado/app/exception/NoBooksDirException.kt create mode 100755 app/src/main/java/io/legado/app/exception/NoStackTraceException.kt create mode 100755 app/src/main/java/io/legado/app/exception/RegexTimeoutException.kt create mode 100755 app/src/main/java/io/legado/app/exception/TocEmptyException.kt create mode 100755 app/src/main/java/io/legado/app/help/AppFreezeMonitor.kt create mode 100755 app/src/main/java/io/legado/app/help/AppWebDav.kt create mode 100755 app/src/main/java/io/legado/app/help/CacheManager.kt create mode 100755 app/src/main/java/io/legado/app/help/ConcurrentRateLimiter.kt create mode 100755 app/src/main/java/io/legado/app/help/CrashHandler.kt create mode 100755 app/src/main/java/io/legado/app/help/DefaultData.kt create mode 100755 app/src/main/java/io/legado/app/help/DirectLinkUpload.kt create mode 100755 app/src/main/java/io/legado/app/help/DispatchersMonitor.kt create mode 100755 app/src/main/java/io/legado/app/help/EventMessage.kt create mode 100755 app/src/main/java/io/legado/app/help/ExecutorService.kt create mode 100755 app/src/main/java/io/legado/app/help/IntentData.kt create mode 100755 app/src/main/java/io/legado/app/help/IntentHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/JsEncodeUtils.kt create mode 100755 app/src/main/java/io/legado/app/help/JsExtensions.kt create mode 100755 app/src/main/java/io/legado/app/help/LauncherIconHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/LayoutManager.kt create mode 100755 app/src/main/java/io/legado/app/help/LifecycleHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/MediaHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/PaintPool.kt create mode 100755 app/src/main/java/io/legado/app/help/README.md create mode 100755 app/src/main/java/io/legado/app/help/ReplaceAnalyzer.kt create mode 100755 app/src/main/java/io/legado/app/help/RuleBigDataHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/RuleComplete.kt create mode 100755 app/src/main/java/io/legado/app/help/TTS.kt create mode 100755 app/src/main/java/io/legado/app/help/book/BookContent.kt create mode 100755 app/src/main/java/io/legado/app/help/book/BookExtensions.kt create mode 100755 app/src/main/java/io/legado/app/help/book/BookHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/book/ContentHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/book/ContentProcessor.kt create mode 100755 app/src/main/java/io/legado/app/help/config/AppConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/config/LocalConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/config/ReadTipConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/config/SourceConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/config/ThemeConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/coroutine/ActivelyCancelException.kt create mode 100755 app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt create mode 100755 app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt create mode 100755 app/src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt create mode 100755 app/src/main/java/io/legado/app/help/crypto/AsymmetricCrypto.kt create mode 100755 app/src/main/java/io/legado/app/help/crypto/README.md create mode 100755 app/src/main/java/io/legado/app/help/crypto/Sign.kt create mode 100755 app/src/main/java/io/legado/app/help/crypto/SymmetricCryptoAndroid.kt create mode 100755 app/src/main/java/io/legado/app/help/exoplayer/ExoPlayerHelper.kt create mode 100755 app/src/main/java/io/legado/app/help/exoplayer/InputStreamDataSource.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/AsyncRecycleBitmapPool.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/BlurTransformation.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/FilePathLoader.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/GlideHeaders.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/ImageLoader.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/LegadoDataUrlLoader.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/OkHttpModeLoaderFactory.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/OkHttpModelLoader.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/progress/OnProgressListener.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/progress/ProgressManager.kt create mode 100755 app/src/main/java/io/legado/app/help/glide/progress/ProgressResponseBody.kt create mode 100755 app/src/main/java/io/legado/app/help/http/BackstageWebView.kt create mode 100755 app/src/main/java/io/legado/app/help/http/CookieManager.kt create mode 100755 app/src/main/java/io/legado/app/help/http/CookieStore.kt create mode 100755 app/src/main/java/io/legado/app/help/http/Cronet.kt create mode 100755 app/src/main/java/io/legado/app/help/http/DecompressInterceptor.kt create mode 100755 app/src/main/java/io/legado/app/help/http/HttpHelper.kt create mode 100755 app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt create mode 100755 app/src/main/java/io/legado/app/help/http/OkHttpExceptionInterceptor.kt create mode 100755 app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt create mode 100755 app/src/main/java/io/legado/app/help/http/OkhttpUncaughtExceptionHandler.kt create mode 100755 app/src/main/java/io/legado/app/help/http/RequestMethod.kt create mode 100755 app/src/main/java/io/legado/app/help/http/SSLHelper.kt create mode 100755 app/src/main/java/io/legado/app/help/http/StrResponse.kt create mode 100755 app/src/main/java/io/legado/app/help/http/api/CookieManagerInterface.kt create mode 100755 app/src/main/java/io/legado/app/help/rhino/NativeBaseSource.kt create mode 100755 app/src/main/java/io/legado/app/help/source/BaseSourceExtensions.kt create mode 100755 app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt create mode 100755 app/src/main/java/io/legado/app/help/source/RssSourceExtensions.kt create mode 100755 app/src/main/java/io/legado/app/help/source/SourceHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/source/SourceVerificationHelp.kt create mode 100755 app/src/main/java/io/legado/app/help/storage/Backup.kt create mode 100755 app/src/main/java/io/legado/app/help/storage/BackupAES.kt create mode 100755 app/src/main/java/io/legado/app/help/storage/BackupConfig.kt create mode 100755 app/src/main/java/io/legado/app/help/storage/ImportOldData.kt create mode 100755 app/src/main/java/io/legado/app/help/storage/Restore.kt create mode 100755 app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt create mode 100755 app/src/main/java/io/legado/app/help/update/AppUpdate.kt create mode 100755 app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt create mode 100755 app/src/main/java/io/legado/app/lib/README.md create mode 100755 app/src/main/java/io/legado/app/lib/aliyun/ALiYun.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/BodyUploadProvider.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/CallbackResult.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/CallbackStep.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/CronetCoroutineInterceptor.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/CronetHelper.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/CronetInterceptor.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/CronetLoader.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/LargeBodyUploadProvider.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/NewCallBack.kt create mode 100755 app/src/main/java/io/legado/app/lib/cronet/OldCallback.kt create mode 100755 app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt create mode 100755 app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt create mode 100755 app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt create mode 100755 app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt create mode 100755 app/src/main/java/io/legado/app/lib/dialogs/SelectItem.kt create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java create mode 100755 app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java create mode 100755 app/src/main/java/io/legado/app/lib/mobi/KF6Book.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/KF8Book.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/MobiBook.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/MobiReader.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/PDBFile.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/decompress/CDICData.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/decompress/Decompressor.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/decompress/HuffcdicDecompressor.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/decompress/Lz77Decompressor.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/decompress/PlainDecompressor.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/ExthRecordType.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/FdstHeader.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/Fragment.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/IndexData.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/IndexEntry.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/IndexTag.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/IndxHeader.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/KF6Section.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/KF8Header.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/KF8Pos.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/KF8Resource.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/KF8Section.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/MobiEntryHeaders.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/MobiHeader.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/MobiMetadata.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/NCX.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/PalmDocHeader.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/Ptagx.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/Skeleton.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/TOC.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/TagxHeader.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/entities/TagxTag.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/utils/BitwiseExtensions.kt create mode 100755 app/src/main/java/io/legado/app/lib/mobi/utils/ByteBufferExtensions.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/OnErrorCallback.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/Permissions.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/Request.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/RequestManager.kt create mode 100755 app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/EditTextPreference.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/IconListPreference.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/NameListPreference.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/Preference.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/PreferenceCategory.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt create mode 100755 app/src/main/java/io/legado/app/lib/prefs/fragment/PreferenceFragment.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/Selector.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/ThemeUtils.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/TintHelper.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeBottomNavigationVIew.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeCheckBox.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeEditText.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeProgressBar.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioButton.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioNoButton.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeSeekBar.kt create mode 100755 app/src/main/java/io/legado/app/lib/theme/view/ThemeSwitch.kt create mode 100755 app/src/main/java/io/legado/app/lib/webdav/Authorization.kt create mode 100755 app/src/main/java/io/legado/app/lib/webdav/WebDav.kt create mode 100755 app/src/main/java/io/legado/app/lib/webdav/WebDavException.kt create mode 100755 app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt create mode 100755 app/src/main/java/io/legado/app/model/AudioPlay.kt create mode 100755 app/src/main/java/io/legado/app/model/BookCover.kt create mode 100755 app/src/main/java/io/legado/app/model/CacheBook.kt create mode 100755 app/src/main/java/io/legado/app/model/CheckSource.kt create mode 100755 app/src/main/java/io/legado/app/model/Debug.kt create mode 100755 app/src/main/java/io/legado/app/model/Download.kt create mode 100755 app/src/main/java/io/legado/app/model/ImageProvider.kt create mode 100755 app/src/main/java/io/legado/app/model/README.md create mode 100755 app/src/main/java/io/legado/app/model/ReadAloud.kt create mode 100755 app/src/main/java/io/legado/app/model/ReadBook.kt create mode 100755 app/src/main/java/io/legado/app/model/ReadManga.kt create mode 100755 app/src/main/java/io/legado/app/model/SharedJsScope.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/CustomUrl.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt create mode 100755 app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/BaseLocalBookParse.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/EpubFile.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/LocalBook.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/MobiFile.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/PdfFile.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/README.md create mode 100755 app/src/main/java/io/legado/app/model/localBook/TextFile.kt create mode 100755 app/src/main/java/io/legado/app/model/localBook/UmdFile.kt create mode 100755 app/src/main/java/io/legado/app/model/remote/RemoteBook.kt create mode 100755 app/src/main/java/io/legado/app/model/remote/RemoteBookManager.kt create mode 100755 app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt create mode 100755 app/src/main/java/io/legado/app/model/rss/Rss.kt create mode 100755 app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt create mode 100755 app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt create mode 100755 app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt create mode 100755 app/src/main/java/io/legado/app/model/webBook/BookContent.kt create mode 100755 app/src/main/java/io/legado/app/model/webBook/BookInfo.kt create mode 100755 app/src/main/java/io/legado/app/model/webBook/BookList.kt create mode 100755 app/src/main/java/io/legado/app/model/webBook/SearchModel.kt create mode 100755 app/src/main/java/io/legado/app/model/webBook/WebBook.kt create mode 100755 app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt create mode 100755 app/src/main/java/io/legado/app/receiver/NetworkChangedListener.kt create mode 100755 app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt create mode 100755 app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt create mode 100755 app/src/main/java/io/legado/app/service/AudioPlayService.kt create mode 100755 app/src/main/java/io/legado/app/service/BaseReadAloudService.kt create mode 100755 app/src/main/java/io/legado/app/service/CacheBookService.kt create mode 100755 app/src/main/java/io/legado/app/service/CheckSourceService.kt create mode 100755 app/src/main/java/io/legado/app/service/DownloadService.kt create mode 100755 app/src/main/java/io/legado/app/service/ExportBookService.kt create mode 100755 app/src/main/java/io/legado/app/service/HttpReadAloudService.kt create mode 100755 app/src/main/java/io/legado/app/service/README.md create mode 100755 app/src/main/java/io/legado/app/service/TTSReadAloudService.kt create mode 100755 app/src/main/java/io/legado/app/service/WebService.kt create mode 100755 app/src/main/java/io/legado/app/service/WebTileService.kt create mode 100755 app/src/main/java/io/legado/app/ui/README.md create mode 100755 app/src/main/java/io/legado/app/ui/about/AboutActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/about/AboutFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/about/AppLogDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/about/CrashLogsDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/about/UpdateDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportHttpTtsDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportThemeDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportThemeViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/VerificationCodeActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/association/VerificationCodeViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/audio/TimerSliderPopup.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDecoration.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterTocAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/BaseImportBookActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/local/ImportBook.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/local/ImportBookAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookSort.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterConfig.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/config/MangaEpaperDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterConfig.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterSettingDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/BaseMangaPage.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/EpaperTransformation.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/GrayscaleTransformation.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/MangaChapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/MangaContent.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/MangaPage.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/entities/ReaderLoading.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/GestureDetectorWithLongTap.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaLayoutManager.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaVH.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/ScrollTimer.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonFrame.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonRecyclerView.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/TextParagraph.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceSort.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleEditDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/browser/WebViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/CheckSourceConfig.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/ConfigTag.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/CoverConfigFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/CoverRuleConfigDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/DirectLinkUploadConfig.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/config/WelcomeConfigFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/dict/DictDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/dict/rule/DictRuleAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/dict/rule/DictRuleEditDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/FileManageActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/FileManageViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/FilePickerDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/FilePickerViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/HandleFileActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/HandleFileContract.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/HandleFileViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/file/utils/FilePickerIcon.java create mode 100755 app/src/main/java/io/legado/app/ui/font/FontAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/login/SourceLoginViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/login/WebViewLoginFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/MainActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/MainFragmentInterface.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/MainViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt create mode 100755 app/src/main/java/io/legado/app/ui/qrcode/ScanResultCallback.kt create mode 100755 app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/ReadRecordDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesFragment.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubAdapter.kt create mode 100755 app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/BatteryView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/NoChildScrollNestedScrollView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/PopupAction.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/ReaderInfoBarView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/SearchView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/TitleBar.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/code/CodeView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/code/CodeViewExtensions.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/CodeDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/UrlOptionDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/VariableDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/dynamiclayout/ViewSwitcher.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/ArcView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/ImageButton.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/HeaderAdapterDataObserver.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/NoChildScrollLinearLayoutManager.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/VerticalDivider.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/ViewPager2Container.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/BevelLabelView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/EditEntity.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/MultilineTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/ScrollMultiAutoCompleteTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/ScrollTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt create mode 100755 app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt create mode 100755 app/src/main/java/io/legado/app/utils/ACache.kt create mode 100755 app/src/main/java/io/legado/app/utils/ActivityExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ActivityResult.kt create mode 100755 app/src/main/java/io/legado/app/utils/ActivityResultContracts.kt create mode 100755 app/src/main/java/io/legado/app/utils/AlphanumComparator.kt create mode 100755 app/src/main/java/io/legado/app/utils/AnimationExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ArchiveUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/AsyncFileHandler.kt create mode 100755 app/src/main/java/io/legado/app/utils/BitmapUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/BookChapterExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ByteArrayExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ChineseUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/CollectionExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ColorUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/ConfigurationExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ConflateLiveData.kt create mode 100755 app/src/main/java/io/legado/app/utils/ConstraintModify.kt create mode 100755 app/src/main/java/io/legado/app/utils/ContextExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ConvertExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/CookieManagerExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/CoroutineExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/CustomExportUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/Debounce.kt create mode 100755 app/src/main/java/io/legado/app/utils/DebugLog.kt create mode 100755 app/src/main/java/io/legado/app/utils/DialogExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/DocumentUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/DrawableUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/EncoderUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/EncodingDetect.kt create mode 100755 app/src/main/java/io/legado/app/utils/EventBusExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/FileDocExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/FileExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/FileUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/FlowExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/FragmentExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/GsonExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/HandlerUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/HtmlFormatter.kt create mode 100755 app/src/main/java/io/legado/app/utils/ImageUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/InputStreamExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/IntentExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/IntentType.kt create mode 100755 app/src/main/java/io/legado/app/utils/JsURL.kt create mode 100755 app/src/main/java/io/legado/app/utils/JsonExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/JsoupExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/LogUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/MD5Utils.kt create mode 100755 app/src/main/java/io/legado/app/utils/MapExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/MenuExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/MenuItemExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/MutableLiveDataExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/NavigationViewUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/NetworkUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/PaintExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/ParcelFileDescriptorChannel.kt create mode 100755 app/src/main/java/io/legado/app/utils/PreferencesExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/QRCodeUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/RandomColor.kt create mode 100755 app/src/main/java/io/legado/app/utils/RealPathUtil.kt create mode 100755 app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/RegexExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/RequestManagerExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/Snackbars.kt create mode 100755 app/src/main/java/io/legado/app/utils/StringExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/StringUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/SvgUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/SyncedRenderer.kt create mode 100755 app/src/main/java/io/legado/app/utils/SystemUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/Throttle.kt create mode 100755 app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/TimeUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/ToastUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/UriExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/UrlUtil.kt create mode 100755 app/src/main/java/io/legado/app/utils/Utf8BomUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/ViewExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/WebSettingsExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/WindowInsetsExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/pools/CanvasPool.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt create mode 100755 app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt create mode 100755 app/src/main/java/io/legado/app/utils/compress/LibArchiveUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt create mode 100755 app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt create mode 100755 app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt create mode 100755 app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt create mode 100755 app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt create mode 100755 app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt create mode 100755 app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt create mode 100755 app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt create mode 100755 app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt create mode 100755 app/src/main/java/io/legado/app/web/HttpServer.kt create mode 100755 app/src/main/java/io/legado/app/web/ReadMe.md create mode 100755 app/src/main/java/io/legado/app/web/WebSocketServer.kt create mode 100755 app/src/main/java/io/legado/app/web/socket/BookSearchWebSocket.kt create mode 100755 app/src/main/java/io/legado/app/web/socket/BookSourceDebugWebSocket.kt create mode 100755 app/src/main/java/io/legado/app/web/socket/RssSourceDebugWebSocket.kt create mode 100755 app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt create mode 100755 app/src/main/res/anim/anim_none.xml create mode 100755 app/src/main/res/anim/anim_readbook_bottom_in.xml create mode 100755 app/src/main/res/anim/anim_readbook_bottom_out.xml create mode 100755 app/src/main/res/anim/anim_readbook_top_in.xml create mode 100755 app/src/main/res/anim/anim_readbook_top_out.xml create mode 100755 app/src/main/res/color/selector_image.xml create mode 100755 app/src/main/res/drawable/bg_chapter_item_divider.xml create mode 100755 app/src/main/res/drawable/bg_edit.xml create mode 100755 app/src/main/res/drawable/bg_eink_border_bottom.xml create mode 100755 app/src/main/res/drawable/bg_eink_border_dialog.xml create mode 100755 app/src/main/res/drawable/bg_eink_border_top.xml create mode 100755 app/src/main/res/drawable/bg_find_book_group.xml create mode 100755 app/src/main/res/drawable/bg_gradient.xml create mode 100755 app/src/main/res/drawable/bg_img_border.xml create mode 100755 app/src/main/res/drawable/bg_item_focused_on_tv.xml create mode 100755 app/src/main/res/drawable/bg_popup_menu.xml create mode 100755 app/src/main/res/drawable/bg_prefs_color.xml create mode 100755 app/src/main/res/drawable/bg_searchview.xml create mode 100755 app/src/main/res/drawable/bg_shadow_bottom.xml create mode 100755 app/src/main/res/drawable/bg_shadow_bottom_night.xml create mode 100755 app/src/main/res/drawable/bg_shadow_top.xml create mode 100755 app/src/main/res/drawable/bg_shadow_top_night.xml create mode 100755 app/src/main/res/drawable/bg_textfield_search.xml create mode 100755 app/src/main/res/drawable/fastscroll_bubble.xml create mode 100755 app/src/main/res/drawable/fastscroll_handle.xml create mode 100755 app/src/main/res/drawable/fastscroll_track.xml create mode 100755 app/src/main/res/drawable/ic_add.xml create mode 100755 app/src/main/res/drawable/ic_add_online.xml create mode 100755 app/src/main/res/drawable/ic_arrange.xml create mode 100755 app/src/main/res/drawable/ic_arrow_back.xml create mode 100755 app/src/main/res/drawable/ic_arrow_down.xml create mode 100755 app/src/main/res/drawable/ic_arrow_drop_down.xml create mode 100755 app/src/main/res/drawable/ic_arrow_drop_up.xml create mode 100755 app/src/main/res/drawable/ic_arrow_right.xml create mode 100755 app/src/main/res/drawable/ic_author.xml create mode 100755 app/src/main/res/drawable/ic_auto_page.xml create mode 100755 app/src/main/res/drawable/ic_auto_page_stop.xml create mode 100755 app/src/main/res/drawable/ic_backup.xml create mode 100755 app/src/main/res/drawable/ic_baseline_close.xml create mode 100755 app/src/main/res/drawable/ic_baseline_sort_24.xml create mode 100755 app/src/main/res/drawable/ic_book_has.xml create mode 100755 app/src/main/res/drawable/ic_book_last.xml create mode 100755 app/src/main/res/drawable/ic_bookmark.xml create mode 100755 app/src/main/res/drawable/ic_bottom_books.xml create mode 100755 app/src/main/res/drawable/ic_bottom_books_e.xml create mode 100755 app/src/main/res/drawable/ic_bottom_books_s.xml create mode 100755 app/src/main/res/drawable/ic_bottom_explore.xml create mode 100755 app/src/main/res/drawable/ic_bottom_explore_e.xml create mode 100755 app/src/main/res/drawable/ic_bottom_explore_s.xml create mode 100755 app/src/main/res/drawable/ic_bottom_person.xml create mode 100755 app/src/main/res/drawable/ic_bottom_person_e.xml create mode 100755 app/src/main/res/drawable/ic_bottom_person_s.xml create mode 100755 app/src/main/res/drawable/ic_bottom_rss_feed.xml create mode 100755 app/src/main/res/drawable/ic_bottom_rss_feed_e.xml create mode 100755 app/src/main/res/drawable/ic_bottom_rss_feed_s.xml create mode 100755 app/src/main/res/drawable/ic_brightness.xml create mode 100755 app/src/main/res/drawable/ic_brightness_auto.xml create mode 100755 app/src/main/res/drawable/ic_bubble_chart.xml create mode 100755 app/src/main/res/drawable/ic_bug_report.xml create mode 100755 app/src/main/res/drawable/ic_cfg_about.xml create mode 100755 app/src/main/res/drawable/ic_cfg_backup.xml create mode 100755 app/src/main/res/drawable/ic_cfg_donate.xml create mode 100755 app/src/main/res/drawable/ic_cfg_other.xml create mode 100755 app/src/main/res/drawable/ic_cfg_replace.xml create mode 100755 app/src/main/res/drawable/ic_cfg_source.xml create mode 100755 app/src/main/res/drawable/ic_cfg_theme.xml create mode 100755 app/src/main/res/drawable/ic_cfg_web.xml create mode 100755 app/src/main/res/drawable/ic_chapter_list.xml create mode 100755 app/src/main/res/drawable/ic_check.xml create mode 100755 app/src/main/res/drawable/ic_check_source.xml create mode 100755 app/src/main/res/drawable/ic_clear_all.xml create mode 100755 app/src/main/res/drawable/ic_copy.xml create mode 100755 app/src/main/res/drawable/ic_create_folder_outline.xml create mode 100755 app/src/main/res/drawable/ic_cursor_left.xml create mode 100755 app/src/main/res/drawable/ic_cursor_right.xml create mode 100755 app/src/main/res/drawable/ic_daytime.xml create mode 100755 app/src/main/res/drawable/ic_divider.xml create mode 100755 app/src/main/res/drawable/ic_download.xml create mode 100755 app/src/main/res/drawable/ic_download_line.xml create mode 100755 app/src/main/res/drawable/ic_edit.xml create mode 100755 app/src/main/res/drawable/ic_exchange.xml create mode 100755 app/src/main/res/drawable/ic_exchange_order.xml create mode 100755 app/src/main/res/drawable/ic_exit.xml create mode 100755 app/src/main/res/drawable/ic_expand_less.xml create mode 100755 app/src/main/res/drawable/ic_expand_more.xml create mode 100755 app/src/main/res/drawable/ic_export.xml create mode 100755 app/src/main/res/drawable/ic_fast_forward.xml create mode 100755 app/src/main/res/drawable/ic_fast_rewind.xml create mode 100755 app/src/main/res/drawable/ic_find_replace.xml create mode 100755 app/src/main/res/drawable/ic_folder.xml create mode 100755 app/src/main/res/drawable/ic_folder_open.xml create mode 100755 app/src/main/res/drawable/ic_folder_outline.xml create mode 100755 app/src/main/res/drawable/ic_groups.xml create mode 100755 app/src/main/res/drawable/ic_help.xml create mode 100755 app/src/main/res/drawable/ic_history.xml create mode 100755 app/src/main/res/drawable/ic_image.xml create mode 100755 app/src/main/res/drawable/ic_import.xml create mode 100755 app/src/main/res/drawable/ic_interface_setting.xml create mode 100755 app/src/main/res/drawable/ic_launcher1.xml create mode 100755 app/src/main/res/drawable/ic_launcher1_b.xml create mode 100755 app/src/main/res/drawable/ic_launcher2.xml create mode 100755 app/src/main/res/drawable/ic_launcher3.xml create mode 100755 app/src/main/res/drawable/ic_launcher4.xml create mode 100755 app/src/main/res/drawable/ic_launcher4_b.xml create mode 100755 app/src/main/res/drawable/ic_launcher5.xml create mode 100755 app/src/main/res/drawable/ic_launcher5_b.xml create mode 100755 app/src/main/res/drawable/ic_launcher6.xml create mode 100755 app/src/main/res/drawable/ic_launcher7.xml create mode 100755 app/src/main/res/drawable/ic_launcher7_b.xml create mode 100755 app/src/main/res/drawable/ic_lock_outline.xml create mode 100755 app/src/main/res/drawable/ic_menu.xml create mode 100755 app/src/main/res/drawable/ic_more.xml create mode 100755 app/src/main/res/drawable/ic_more_vert.xml create mode 100755 app/src/main/res/drawable/ic_network_check.xml create mode 100755 app/src/main/res/drawable/ic_outline_cloud_24.xml create mode 100755 app/src/main/res/drawable/ic_outline_delete.xml create mode 100755 app/src/main/res/drawable/ic_pause_24dp.xml create mode 100755 app/src/main/res/drawable/ic_pause_outline_24dp.xml create mode 100755 app/src/main/res/drawable/ic_play_24dp.xml create mode 100755 app/src/main/res/drawable/ic_play_mode_list_end_stop.xml create mode 100755 app/src/main/res/drawable/ic_play_mode_list_loop.xml create mode 100755 app/src/main/res/drawable/ic_play_mode_random.xml create mode 100755 app/src/main/res/drawable/ic_play_mode_single_loop.xml create mode 100755 app/src/main/res/drawable/ic_play_outline_24dp.xml create mode 100755 app/src/main/res/drawable/ic_praise.xml create mode 100755 app/src/main/res/drawable/ic_read_aloud.xml create mode 100755 app/src/main/res/drawable/ic_reduce.xml create mode 100755 app/src/main/res/drawable/ic_refresh_black_24dp.xml create mode 100755 app/src/main/res/drawable/ic_refresh_white_24dp.xml create mode 100755 app/src/main/res/drawable/ic_restore.xml create mode 100755 app/src/main/res/drawable/ic_save.xml create mode 100755 app/src/main/res/drawable/ic_scan.xml create mode 100755 app/src/main/res/drawable/ic_scoring.xml create mode 100755 app/src/main/res/drawable/ic_screen.xml create mode 100755 app/src/main/res/drawable/ic_search.xml create mode 100755 app/src/main/res/drawable/ic_search_hint.xml create mode 100755 app/src/main/res/drawable/ic_settings.xml create mode 100755 app/src/main/res/drawable/ic_share.xml create mode 100755 app/src/main/res/drawable/ic_skip_next.xml create mode 100755 app/src/main/res/drawable/ic_skip_previous.xml create mode 100755 app/src/main/res/drawable/ic_sort.xml create mode 100755 app/src/main/res/drawable/ic_star.xml create mode 100755 app/src/main/res/drawable/ic_star_border.xml create mode 100755 app/src/main/res/drawable/ic_stop_black_24dp.xml create mode 100755 app/src/main/res/drawable/ic_storage_black_24dp.xml create mode 100755 app/src/main/res/drawable/ic_swap_horiz.xml create mode 100755 app/src/main/res/drawable/ic_time_add_24dp.xml create mode 100755 app/src/main/res/drawable/ic_timer_black_24dp.xml create mode 100755 app/src/main/res/drawable/ic_toc.xml create mode 100755 app/src/main/res/drawable/ic_translate.xml create mode 100755 app/src/main/res/drawable/ic_update.xml create mode 100755 app/src/main/res/drawable/ic_view_quilt.xml create mode 100755 app/src/main/res/drawable/ic_visibility_off.xml create mode 100755 app/src/main/res/drawable/ic_volume_up.xml create mode 100755 app/src/main/res/drawable/ic_web_outline.xml create mode 100755 app/src/main/res/drawable/ic_web_service_noti.xml create mode 100755 app/src/main/res/drawable/icon_read_book.png create mode 100755 app/src/main/res/drawable/image_cover_default.jpg create mode 100755 app/src/main/res/drawable/image_legado.png create mode 100755 app/src/main/res/drawable/image_loading_error.png create mode 100755 app/src/main/res/drawable/image_rss.jpg create mode 100755 app/src/main/res/drawable/image_rss_article.jpg create mode 100755 app/src/main/res/drawable/recyclerview_divider_horizontal.xml create mode 100755 app/src/main/res/drawable/recyclerview_divider_vertical.xml create mode 100755 app/src/main/res/drawable/selector_btn_accent_bg.xml create mode 100755 app/src/main/res/drawable/selector_circle_btn_bg.xml create mode 100755 app/src/main/res/drawable/selector_common_bg.xml create mode 100755 app/src/main/res/drawable/selector_fillet_btn_bg.xml create mode 100755 app/src/main/res/drawable/selector_tv_black.xml create mode 100755 app/src/main/res/drawable/shape_card_view.xml create mode 100755 app/src/main/res/drawable/shape_circle.xml create mode 100755 app/src/main/res/drawable/shape_fillet_btn.xml create mode 100755 app/src/main/res/drawable/shape_fillet_btn_press.xml create mode 100755 app/src/main/res/drawable/shape_pop_checkaddshelf_bg.xml create mode 100755 app/src/main/res/drawable/shape_radius_10dp.xml create mode 100755 app/src/main/res/drawable/shape_radius_1dp.xml create mode 100755 app/src/main/res/drawable/shape_space_divider.xml create mode 100755 app/src/main/res/drawable/shape_text_cursor.xml create mode 100755 app/src/main/res/drawable/shape_translucent_card.xml create mode 100755 app/src/main/res/layout-land/activity_book_info.xml create mode 100755 app/src/main/res/layout/activity_about.xml create mode 100755 app/src/main/res/layout/activity_all_bookmark.xml create mode 100755 app/src/main/res/layout/activity_arrange_book.xml create mode 100755 app/src/main/res/layout/activity_audio_play.xml create mode 100755 app/src/main/res/layout/activity_book_info.xml create mode 100755 app/src/main/res/layout/activity_book_info_edit.xml create mode 100755 app/src/main/res/layout/activity_book_read.xml create mode 100755 app/src/main/res/layout/activity_book_search.xml create mode 100755 app/src/main/res/layout/activity_book_source.xml create mode 100755 app/src/main/res/layout/activity_book_source_edit.xml create mode 100755 app/src/main/res/layout/activity_cache_book.xml create mode 100755 app/src/main/res/layout/activity_chapter_list.xml create mode 100755 app/src/main/res/layout/activity_config.xml create mode 100755 app/src/main/res/layout/activity_dict_rule.xml create mode 100755 app/src/main/res/layout/activity_donate.xml create mode 100755 app/src/main/res/layout/activity_explore_show.xml create mode 100755 app/src/main/res/layout/activity_file_manage.xml create mode 100755 app/src/main/res/layout/activity_import_book.xml create mode 100755 app/src/main/res/layout/activity_main.xml create mode 100755 app/src/main/res/layout/activity_manga.xml create mode 100755 app/src/main/res/layout/activity_qrcode_capture.xml create mode 100755 app/src/main/res/layout/activity_read_record.xml create mode 100755 app/src/main/res/layout/activity_replace_edit.xml create mode 100755 app/src/main/res/layout/activity_replace_rule.xml create mode 100755 app/src/main/res/layout/activity_rss_artivles.xml create mode 100755 app/src/main/res/layout/activity_rss_favorites.xml create mode 100755 app/src/main/res/layout/activity_rss_read.xml create mode 100755 app/src/main/res/layout/activity_rss_source.xml create mode 100755 app/src/main/res/layout/activity_rss_source_edit.xml create mode 100755 app/src/main/res/layout/activity_rule_sub.xml create mode 100755 app/src/main/res/layout/activity_search_content.xml create mode 100755 app/src/main/res/layout/activity_source_debug.xml create mode 100755 app/src/main/res/layout/activity_source_login.xml create mode 100755 app/src/main/res/layout/activity_translucence.xml create mode 100755 app/src/main/res/layout/activity_txt_toc_rule.xml create mode 100755 app/src/main/res/layout/activity_web_view.xml create mode 100755 app/src/main/res/layout/activity_welcome.xml create mode 100755 app/src/main/res/layout/dialog_add_to_bookshelf.xml create mode 100755 app/src/main/res/layout/dialog_auto_read.xml create mode 100755 app/src/main/res/layout/dialog_book_change_source.xml create mode 100755 app/src/main/res/layout/dialog_book_group_edit.xml create mode 100755 app/src/main/res/layout/dialog_book_group_picker.xml create mode 100755 app/src/main/res/layout/dialog_bookmark.xml create mode 100755 app/src/main/res/layout/dialog_bookshelf_config.xml create mode 100755 app/src/main/res/layout/dialog_change_cover.xml create mode 100755 app/src/main/res/layout/dialog_chapter_change_source.xml create mode 100755 app/src/main/res/layout/dialog_check_source_config.xml create mode 100755 app/src/main/res/layout/dialog_click_action_config.xml create mode 100755 app/src/main/res/layout/dialog_code_view.xml create mode 100755 app/src/main/res/layout/dialog_content_edit.xml create mode 100755 app/src/main/res/layout/dialog_cover_rule_config.xml create mode 100755 app/src/main/res/layout/dialog_custom_group.xml create mode 100755 app/src/main/res/layout/dialog_dict.xml create mode 100755 app/src/main/res/layout/dialog_dict_rule_edit.xml create mode 100755 app/src/main/res/layout/dialog_direct_link_upload_config.xml create mode 100755 app/src/main/res/layout/dialog_download_choice.xml create mode 100755 app/src/main/res/layout/dialog_edit_text.xml create mode 100755 app/src/main/res/layout/dialog_file_chooser.xml create mode 100755 app/src/main/res/layout/dialog_font_select.xml create mode 100755 app/src/main/res/layout/dialog_http_tts_edit.xml create mode 100755 app/src/main/res/layout/dialog_image_blurring.xml create mode 100755 app/src/main/res/layout/dialog_login.xml create mode 100755 app/src/main/res/layout/dialog_manga_color_filter.xml create mode 100755 app/src/main/res/layout/dialog_manga_epaper.xml create mode 100755 app/src/main/res/layout/dialog_manga_footer_setting.xml create mode 100755 app/src/main/res/layout/dialog_multiple_edit_text.xml create mode 100755 app/src/main/res/layout/dialog_number_picker.xml create mode 100755 app/src/main/res/layout/dialog_open_url_confirm.xml create mode 100755 app/src/main/res/layout/dialog_page_key.xml create mode 100755 app/src/main/res/layout/dialog_photo_view.xml create mode 100755 app/src/main/res/layout/dialog_progressbar_view.xml create mode 100755 app/src/main/res/layout/dialog_read_aloud.xml create mode 100755 app/src/main/res/layout/dialog_read_bg_text.xml create mode 100755 app/src/main/res/layout/dialog_read_book_style.xml create mode 100755 app/src/main/res/layout/dialog_read_padding.xml create mode 100755 app/src/main/res/layout/dialog_recycler_view.xml create mode 100755 app/src/main/res/layout/dialog_rss_favorite_config.xml create mode 100755 app/src/main/res/layout/dialog_rule_sub_edit.xml create mode 100755 app/src/main/res/layout/dialog_search_scope.xml create mode 100755 app/src/main/res/layout/dialog_select_section_export.xml create mode 100755 app/src/main/res/layout/dialog_simulated_reading.xml create mode 100755 app/src/main/res/layout/dialog_source_picker.xml create mode 100755 app/src/main/res/layout/dialog_text_view.xml create mode 100755 app/src/main/res/layout/dialog_tip_config.xml create mode 100755 app/src/main/res/layout/dialog_toc_regex.xml create mode 100755 app/src/main/res/layout/dialog_toc_regex_edit.xml create mode 100755 app/src/main/res/layout/dialog_update.xml create mode 100755 app/src/main/res/layout/dialog_url_option_edit.xml create mode 100755 app/src/main/res/layout/dialog_variable.xml create mode 100755 app/src/main/res/layout/dialog_verification_code_view.xml create mode 100755 app/src/main/res/layout/dialog_wait.xml create mode 100755 app/src/main/res/layout/dialog_webdav_server.xml create mode 100755 app/src/main/res/layout/fragment_bookmark.xml create mode 100755 app/src/main/res/layout/fragment_books.xml create mode 100755 app/src/main/res/layout/fragment_bookshelf1.xml create mode 100755 app/src/main/res/layout/fragment_bookshelf2.xml create mode 100755 app/src/main/res/layout/fragment_chapter_list.xml create mode 100755 app/src/main/res/layout/fragment_explore.xml create mode 100755 app/src/main/res/layout/fragment_my_config.xml create mode 100755 app/src/main/res/layout/fragment_rss.xml create mode 100755 app/src/main/res/layout/fragment_rss_articles.xml create mode 100755 app/src/main/res/layout/fragment_web_view_login.xml create mode 100755 app/src/main/res/layout/item_1line_text.xml create mode 100755 app/src/main/res/layout/item_1line_text_and_del.xml create mode 100755 app/src/main/res/layout/item_app_log.xml create mode 100755 app/src/main/res/layout/item_arrange_book.xml create mode 100755 app/src/main/res/layout/item_bg_image.xml create mode 100755 app/src/main/res/layout/item_book_file_import.xml create mode 100755 app/src/main/res/layout/item_book_group_manage.xml create mode 100755 app/src/main/res/layout/item_book_manga_edge.xml create mode 100755 app/src/main/res/layout/item_book_manga_page.xml create mode 100755 app/src/main/res/layout/item_book_source.xml create mode 100755 app/src/main/res/layout/item_bookmark.xml create mode 100755 app/src/main/res/layout/item_bookshelf_grid.xml create mode 100755 app/src/main/res/layout/item_bookshelf_grid_group.xml create mode 100755 app/src/main/res/layout/item_bookshelf_list.xml create mode 100755 app/src/main/res/layout/item_bookshelf_list_group.xml create mode 100755 app/src/main/res/layout/item_change_source.xml create mode 100755 app/src/main/res/layout/item_chapter_list.xml create mode 100755 app/src/main/res/layout/item_check_box.xml create mode 100755 app/src/main/res/layout/item_cover.xml create mode 100755 app/src/main/res/layout/item_dict_rule.xml create mode 100755 app/src/main/res/layout/item_download.xml create mode 100755 app/src/main/res/layout/item_file.xml create mode 100755 app/src/main/res/layout/item_file_picker.xml create mode 100755 app/src/main/res/layout/item_fillet_text.xml create mode 100755 app/src/main/res/layout/item_find_book.xml create mode 100755 app/src/main/res/layout/item_font.xml create mode 100755 app/src/main/res/layout/item_group_manage.xml create mode 100755 app/src/main/res/layout/item_group_select.xml create mode 100755 app/src/main/res/layout/item_http_tts.xml create mode 100755 app/src/main/res/layout/item_icon_preference.xml create mode 100755 app/src/main/res/layout/item_import_book.xml create mode 100755 app/src/main/res/layout/item_log.xml create mode 100755 app/src/main/res/layout/item_path_picker.xml create mode 100755 app/src/main/res/layout/item_radio_button.xml create mode 100755 app/src/main/res/layout/item_read_record.xml create mode 100755 app/src/main/res/layout/item_read_style.xml create mode 100755 app/src/main/res/layout/item_replace_rule.xml create mode 100755 app/src/main/res/layout/item_rss.xml create mode 100755 app/src/main/res/layout/item_rss_article.xml create mode 100755 app/src/main/res/layout/item_rss_article_1.xml create mode 100755 app/src/main/res/layout/item_rss_article_2.xml create mode 100755 app/src/main/res/layout/item_rss_read_record.xml create mode 100755 app/src/main/res/layout/item_rss_source.xml create mode 100755 app/src/main/res/layout/item_rule_sub.xml create mode 100755 app/src/main/res/layout/item_search.xml create mode 100755 app/src/main/res/layout/item_search_list.xml create mode 100755 app/src/main/res/layout/item_server_select.xml create mode 100755 app/src/main/res/layout/item_source_edit.xml create mode 100755 app/src/main/res/layout/item_source_edit_check_box.xml create mode 100755 app/src/main/res/layout/item_source_import.xml create mode 100755 app/src/main/res/layout/item_text.xml create mode 100755 app/src/main/res/layout/item_theme_config.xml create mode 100755 app/src/main/res/layout/item_toc_regex.xml create mode 100755 app/src/main/res/layout/item_txt_toc_rule.xml create mode 100755 app/src/main/res/layout/popup_action.xml create mode 100755 app/src/main/res/layout/popup_action_menu.xml create mode 100755 app/src/main/res/layout/popup_keyboard_tool.xml create mode 100755 app/src/main/res/layout/popup_seek_bar.xml create mode 100755 app/src/main/res/layout/view_action_button.xml create mode 100755 app/src/main/res/layout/view_book_page.xml create mode 100755 app/src/main/res/layout/view_detail_seek_bar.xml create mode 100755 app/src/main/res/layout/view_dynamic.xml create mode 100755 app/src/main/res/layout/view_error.xml create mode 100755 app/src/main/res/layout/view_fastscroller.xml create mode 100755 app/src/main/res/layout/view_icon.xml create mode 100755 app/src/main/res/layout/view_load_more.xml create mode 100755 app/src/main/res/layout/view_loading.xml create mode 100755 app/src/main/res/layout/view_manga_menu.xml create mode 100755 app/src/main/res/layout/view_navigation_badge.xml create mode 100755 app/src/main/res/layout/view_preference.xml create mode 100755 app/src/main/res/layout/view_preference_category.xml create mode 100755 app/src/main/res/layout/view_read_menu.xml create mode 100755 app/src/main/res/layout/view_refresh_recycler.xml create mode 100755 app/src/main/res/layout/view_search.xml create mode 100755 app/src/main/res/layout/view_search_menu.xml create mode 100755 app/src/main/res/layout/view_select_action_bar.xml create mode 100755 app/src/main/res/layout/view_tab_layout.xml create mode 100755 app/src/main/res/layout/view_tab_layout_min.xml create mode 100755 app/src/main/res/layout/view_title_bar.xml create mode 100755 app/src/main/res/layout/view_title_bar_dark.xml create mode 100755 app/src/main/res/layout/view_toast.xml create mode 100755 app/src/main/res/menu/about.xml create mode 100755 app/src/main/res/menu/app_log.xml create mode 100755 app/src/main/res/menu/app_update.xml create mode 100755 app/src/main/res/menu/audio_play.xml create mode 100755 app/src/main/res/menu/backup_restore.xml create mode 100755 app/src/main/res/menu/book_cache.xml create mode 100755 app/src/main/res/menu/book_cache_download.xml create mode 100755 app/src/main/res/menu/book_group_manage.xml create mode 100755 app/src/main/res/menu/book_info.xml create mode 100755 app/src/main/res/menu/book_info_edit.xml create mode 100755 app/src/main/res/menu/book_manga.xml create mode 100755 app/src/main/res/menu/book_read.xml create mode 100755 app/src/main/res/menu/book_read_change_source.xml create mode 100755 app/src/main/res/menu/book_read_record.xml create mode 100755 app/src/main/res/menu/book_read_refresh.xml create mode 100755 app/src/main/res/menu/book_read_source.xml create mode 100755 app/src/main/res/menu/book_remote.xml create mode 100755 app/src/main/res/menu/book_search.xml create mode 100755 app/src/main/res/menu/book_search_scope.xml create mode 100755 app/src/main/res/menu/book_source.xml create mode 100755 app/src/main/res/menu/book_source_debug.xml create mode 100755 app/src/main/res/menu/book_source_item.xml create mode 100755 app/src/main/res/menu/book_source_sel.xml create mode 100755 app/src/main/res/menu/book_toc.xml create mode 100755 app/src/main/res/menu/bookmark.xml create mode 100755 app/src/main/res/menu/bookshelf_manage.xml create mode 100755 app/src/main/res/menu/bookshelf_menage_sel.xml create mode 100755 app/src/main/res/menu/change_cover.xml create mode 100755 app/src/main/res/menu/change_source.xml create mode 100755 app/src/main/res/menu/change_source_item.xml create mode 100755 app/src/main/res/menu/code_edit.xml create mode 100755 app/src/main/res/menu/content_edit.xml create mode 100755 app/src/main/res/menu/content_search.xml create mode 100755 app/src/main/res/menu/content_select_action.xml create mode 100755 app/src/main/res/menu/crash_log.xml create mode 100755 app/src/main/res/menu/dialog_text.xml create mode 100755 app/src/main/res/menu/dict_rule.xml create mode 100755 app/src/main/res/menu/dict_rule_edit.xml create mode 100755 app/src/main/res/menu/dict_rule_sel.xml create mode 100755 app/src/main/res/menu/direct_link_upload_config.xml create mode 100755 app/src/main/res/menu/explore_item.xml create mode 100755 app/src/main/res/menu/file_chooser.xml create mode 100755 app/src/main/res/menu/file_long_click.xml create mode 100755 app/src/main/res/menu/font_select.xml create mode 100755 app/src/main/res/menu/group_manage.xml create mode 100755 app/src/main/res/menu/import_book.xml create mode 100755 app/src/main/res/menu/import_book_sel.xml create mode 100755 app/src/main/res/menu/import_replace.xml create mode 100755 app/src/main/res/menu/import_source.xml create mode 100755 app/src/main/res/menu/keyboard_assists_config.xml create mode 100755 app/src/main/res/menu/main_bnv.xml create mode 100755 app/src/main/res/menu/main_bookshelf.xml create mode 100755 app/src/main/res/menu/main_explore.xml create mode 100755 app/src/main/res/menu/main_my.xml create mode 100755 app/src/main/res/menu/main_rss.xml create mode 100755 app/src/main/res/menu/open_url_confirm.xml create mode 100755 app/src/main/res/menu/qr_code_scan.xml create mode 100755 app/src/main/res/menu/replace_edit.xml create mode 100755 app/src/main/res/menu/replace_rule.xml create mode 100755 app/src/main/res/menu/replace_rule_item.xml create mode 100755 app/src/main/res/menu/replace_rule_sel.xml create mode 100755 app/src/main/res/menu/rss_articles.xml create mode 100755 app/src/main/res/menu/rss_favorites.xml create mode 100755 app/src/main/res/menu/rss_main_item.xml create mode 100755 app/src/main/res/menu/rss_read.xml create mode 100755 app/src/main/res/menu/rss_read_record.xml create mode 100755 app/src/main/res/menu/rss_source.xml create mode 100755 app/src/main/res/menu/rss_source_debug.xml create mode 100755 app/src/main/res/menu/rss_source_item.xml create mode 100755 app/src/main/res/menu/rss_source_sel.xml create mode 100755 app/src/main/res/menu/save.xml create mode 100755 app/src/main/res/menu/search_view.xml create mode 100755 app/src/main/res/menu/server_config.xml create mode 100755 app/src/main/res/menu/servers.xml create mode 100755 app/src/main/res/menu/source_edit.xml create mode 100755 app/src/main/res/menu/source_login.xml create mode 100755 app/src/main/res/menu/source_picker.xml create mode 100755 app/src/main/res/menu/source_sub_item.xml create mode 100755 app/src/main/res/menu/source_subscription.xml create mode 100755 app/src/main/res/menu/source_webview_login.xml create mode 100755 app/src/main/res/menu/speak_engine.xml create mode 100755 app/src/main/res/menu/speak_engine_edit.xml create mode 100755 app/src/main/res/menu/theme_config.xml create mode 100755 app/src/main/res/menu/theme_list.xml create mode 100755 app/src/main/res/menu/txt_toc_rule.xml create mode 100755 app/src/main/res/menu/txt_toc_rule_edit.xml create mode 100755 app/src/main/res/menu/txt_toc_rule_item.xml create mode 100755 app/src/main/res/menu/txt_toc_rule_sel.xml create mode 100755 app/src/main/res/menu/verification_code.xml create mode 100755 app/src/main/res/menu/web_view.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/launcher1.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/launcher2.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/launcher3.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/launcher4.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/launcher5.xml create mode 100755 app/src/main/res/mipmap-anydpi-v26/launcher6.xml create mode 100755 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100755 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100755 app/src/main/res/raw/silent_sound.mp3 create mode 100755 app/src/main/res/values-es-rES/arrays.xml create mode 100755 app/src/main/res/values-es-rES/strings.xml create mode 100755 app/src/main/res/values-ja-rJP/strings.xml create mode 100755 app/src/main/res/values-night/colors.xml create mode 100755 app/src/main/res/values-night/styles.xml create mode 100755 app/src/main/res/values-pt-rBR/arrays.xml create mode 100755 app/src/main/res/values-pt-rBR/strings.xml create mode 100755 app/src/main/res/values-vi/arrays.xml create mode 100755 app/src/main/res/values-vi/strings.xml create mode 100755 app/src/main/res/values-zh-rHK/arrays.xml create mode 100755 app/src/main/res/values-zh-rHK/strings.xml create mode 100755 app/src/main/res/values-zh-rTW/arrays.xml create mode 100755 app/src/main/res/values-zh-rTW/strings.xml create mode 100755 app/src/main/res/values-zh/arrays.xml create mode 100755 app/src/main/res/values-zh/strings.xml create mode 100755 app/src/main/res/values/array_values.xml create mode 100755 app/src/main/res/values/arrays.xml create mode 100755 app/src/main/res/values/attrs.xml create mode 100755 app/src/main/res/values/colors.xml create mode 100755 app/src/main/res/values/colors_material_design.xml create mode 100755 app/src/main/res/values/dimens.xml create mode 100755 app/src/main/res/values/ids.xml create mode 100755 app/src/main/res/values/non_translat.xml create mode 100755 app/src/main/res/values/strings.xml create mode 100755 app/src/main/res/values/styles.xml create mode 100755 app/src/main/res/xml/about.xml create mode 100755 app/src/main/res/xml/file_paths.xml create mode 100755 app/src/main/res/xml/network_security_config.xml create mode 100755 app/src/main/res/xml/pref_config_aloud.xml create mode 100755 app/src/main/res/xml/pref_config_backup.xml create mode 100755 app/src/main/res/xml/pref_config_cover.xml create mode 100755 app/src/main/res/xml/pref_config_other.xml create mode 100755 app/src/main/res/xml/pref_config_read.xml create mode 100755 app/src/main/res/xml/pref_config_theme.xml create mode 100755 app/src/main/res/xml/pref_config_welcome.xml create mode 100755 app/src/main/res/xml/pref_main.xml create mode 100755 app/src/main/res/xml/spen_remote_actions.xml create mode 100755 app/src/test/java/io/legado/app/ExampleUnitTest.kt create mode 100755 app/src/test/java/io/legado/app/JsTest.kt create mode 100755 avd.bat create mode 100755 avd.sh create mode 100755 build.gradle create mode 100755 gradle.properties create mode 100755 gradle/libs.versions.toml create mode 100755 gradle/wrapper/gradle-wrapper.jar create mode 100755 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100755 gradlew.bat create mode 100755 modules/book/.gitignore create mode 100755 modules/book/build.gradle create mode 100755 modules/book/consumer-rules.pro create mode 100755 modules/book/src/main/AndroidManifest.xml create mode 100755 modules/book/src/main/java/me/ag2s/base/PfdHelper.java create mode 100755 modules/book/src/main/java/me/ag2s/base/ThrowableUtils.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/Constants.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/browsersupport/NavigationEvent.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/browsersupport/NavigationHistory.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/browsersupport/Navigator.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/browsersupport/package-info.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Author.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Date.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/EpubBook.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Guide.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/GuideReference.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Identifier.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/LazyResource.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/ManifestProperties.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/MediaType.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/MediaTypes.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Metadata.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Relator.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Resource.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/ResourceReference.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Resources.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/Spine.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/SpineReference.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/TOCReference.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/TableOfContents.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/domain/TitledResourceReference.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/BookProcessor.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/DOMUtil.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/EpubProcessorSupport.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/EpubReader.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriter.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/EpubWriterProcessor.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/NCXDocumentV2.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/NCXDocumentV3.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/PackageDocumentBase.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataReader.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/PackageDocumentMetadataWriter.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/PackageDocumentReader.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/PackageDocumentWriter.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/epub/ResourcesLoader.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/CollectionUtil.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/IOUtil.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/NoCloseWriter.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/ResourceUtil.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/StringUtil.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/URLEncodeUtil.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/commons/io/BOMInputStream.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReader.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/commons/io/XmlStreamReaderException.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/zip/AndroidZipEntry.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/zip/AndroidZipFile.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/zip/ZipConstants.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/zip/ZipEntryWrapper.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/zip/ZipException.java create mode 100755 modules/book/src/main/java/me/ag2s/epublib/util/zip/ZipFileWrapper.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/domain/UmdBook.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/domain/UmdChapters.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/domain/UmdCover.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/domain/UmdEnd.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/domain/UmdHeader.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/tool/StreamReader.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/tool/UmdUtils.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java create mode 100755 modules/book/src/main/java/me/ag2s/umdlib/umd/UmdReader.java create mode 100755 modules/book/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oeb12.ent create mode 100755 modules/book/src/main/resources/dtd/openebook.org/dtds/oeb-1.2/oebpkg12.dtd create mode 100755 modules/book/src/main/resources/dtd/www.daisy.org/z3986/2005/ncx-2005-1.dtd create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/ruby/xhtml-ruby-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-attribs-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkphras-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-csismap-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-datatypes-1.mod.1 create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-events-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-form-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-framework-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlphras-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlpres-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-lat1.ent create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-list-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-notations-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-qname-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-special.ent create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-struct-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-symbol.ent.1 create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-table-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml11-model-1.mod create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-special.ent create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd create mode 100755 modules/book/src/main/resources/dtd/www.w3.org/TR/xhtml11/DTD/xhtml11.dtd create mode 100755 modules/book/src/main/resources/log4j.properties create mode 100755 modules/rhino/build.gradle create mode 100755 modules/rhino/consumer-rules.pro create mode 100755 modules/rhino/lib/rhino-1.7.14.jar create mode 100755 modules/rhino/src/main/AndroidManifest.xml create mode 100755 modules/rhino/src/main/java/com/script/AbstractScriptEngine.kt create mode 100755 modules/rhino/src/main/java/com/script/Bindings.kt create mode 100755 modules/rhino/src/main/java/com/script/Compilable.kt create mode 100755 modules/rhino/src/main/java/com/script/CompiledScript.kt create mode 100755 modules/rhino/src/main/java/com/script/Invocable.kt create mode 100755 modules/rhino/src/main/java/com/script/RhinoContextFactory.kt create mode 100755 modules/rhino/src/main/java/com/script/ScriptBindings.kt create mode 100755 modules/rhino/src/main/java/com/script/ScriptBindingsExtensions.kt create mode 100755 modules/rhino/src/main/java/com/script/ScriptContext.kt create mode 100755 modules/rhino/src/main/java/com/script/ScriptEngine.kt create mode 100755 modules/rhino/src/main/java/com/script/ScriptException.kt create mode 100755 modules/rhino/src/main/java/com/script/SimpleBindings.kt create mode 100755 modules/rhino/src/main/java/com/script/SimpleScriptContext.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/ClassNameMatcher.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/CollectionExtensions.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/ExternalScriptable.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/InterfaceImplementor.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/JSAdapter.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/JavaAdapter.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/JavaObjectWrapFactory.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/ProtectedNativeJavaClass.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/ReadOnlyJavaObject.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoClassShutter.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoCompiledScript.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoContext.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoErrors.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoExtensions.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoScriptEngine.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoTopLevel.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/RhinoWrapFactory.kt create mode 100755 modules/rhino/src/main/java/com/script/rhino/VMBridgeReflect.kt create mode 100755 modules/web/.browserslistrc create mode 100755 modules/web/.editorconfig create mode 100755 modules/web/.gitignore create mode 100755 modules/web/.prettierignore create mode 100755 modules/web/.prettierrc.json create mode 100755 modules/web/LICENSE create mode 100755 modules/web/README.md create mode 100755 modules/web/env.d.ts create mode 100755 modules/web/eslint.config.mjs create mode 100755 modules/web/index.html create mode 100755 modules/web/package.json create mode 100755 modules/web/public/favicon.ico create mode 100755 modules/web/scripts/sync.js create mode 100755 modules/web/src/App.vue create mode 100755 modules/web/src/api/api.ts create mode 100755 modules/web/src/api/axios.ts create mode 100755 modules/web/src/api/index.ts create mode 100755 modules/web/src/assets/bookshelf.css create mode 100755 modules/web/src/assets/code.css create mode 100755 modules/web/src/assets/fonts/iconfont.css create mode 100755 modules/web/src/assets/fonts/iconfont.woff create mode 100755 modules/web/src/assets/fonts/popfont.css create mode 100755 modules/web/src/assets/fonts/popfont.ttf create mode 100755 modules/web/src/assets/fonts/shelffont.css create mode 100755 modules/web/src/assets/fonts/shelffont.ttf create mode 100755 modules/web/src/assets/imgs/github.png create mode 100755 modules/web/src/assets/imgs/themes/body_0.png create mode 100755 modules/web/src/assets/imgs/themes/body_1.png create mode 100755 modules/web/src/assets/imgs/themes/body_2.png create mode 100755 modules/web/src/assets/imgs/themes/body_3.png create mode 100755 modules/web/src/assets/imgs/themes/body_5.png create mode 100755 modules/web/src/assets/imgs/themes/body_6.png create mode 100755 modules/web/src/assets/imgs/themes/content_0.png create mode 100755 modules/web/src/assets/imgs/themes/content_1.png create mode 100755 modules/web/src/assets/imgs/themes/content_2.png create mode 100755 modules/web/src/assets/imgs/themes/content_3.png create mode 100755 modules/web/src/assets/imgs/themes/content_5.png create mode 100755 modules/web/src/assets/imgs/themes/content_6.png create mode 100755 modules/web/src/assets/imgs/themes/popup_0.png create mode 100755 modules/web/src/assets/imgs/themes/popup_1.png create mode 100755 modules/web/src/assets/imgs/themes/popup_2.png create mode 100755 modules/web/src/assets/imgs/themes/popup_3.png create mode 100755 modules/web/src/assets/imgs/themes/popup_5.png create mode 100755 modules/web/src/assets/imgs/themes/popup_6.png create mode 100755 modules/web/src/assets/kbd.css create mode 100755 modules/web/src/assets/sourceeditor.css create mode 100755 modules/web/src/auto-imports.d.ts create mode 100755 modules/web/src/book.d.ts create mode 100755 modules/web/src/components.d.ts create mode 100755 modules/web/src/components/BookItems.vue create mode 100755 modules/web/src/components/CatalogItem.vue create mode 100755 modules/web/src/components/ChapterContent.vue create mode 100755 modules/web/src/components/PopCatalog.vue create mode 100755 modules/web/src/components/ReadSettings.vue create mode 100755 modules/web/src/components/SourceDebug.vue create mode 100755 modules/web/src/components/SourceHelp.vue create mode 100755 modules/web/src/components/SourceItem.vue create mode 100755 modules/web/src/components/SourceJson.vue create mode 100755 modules/web/src/components/SourceList.vue create mode 100755 modules/web/src/components/SourceTabForm.vue create mode 100755 modules/web/src/components/SourceTabTools.vue create mode 100755 modules/web/src/components/ToolBar.vue create mode 100755 modules/web/src/config/bookSourceEditConfig.ts create mode 100755 modules/web/src/config/rssSourceEditConfig.ts create mode 100755 modules/web/src/config/sourceConfig.d.ts create mode 100755 modules/web/src/config/themeConfig.ts create mode 100755 modules/web/src/hooks/loading.css create mode 100755 modules/web/src/hooks/loading.ts create mode 100755 modules/web/src/main.ts create mode 100755 modules/web/src/pages/bookshelf/README.md create mode 100755 modules/web/src/pages/bookshelf/index.html create mode 100755 modules/web/src/pages/bookshelf/main.js create mode 100755 modules/web/src/pages/source/README.md create mode 100755 modules/web/src/pages/source/index.html create mode 100755 modules/web/src/pages/source/main.js create mode 100755 modules/web/src/plugins/jump.d.ts create mode 100755 modules/web/src/plugins/jump.js create mode 100755 modules/web/src/router/bookRouter.ts create mode 100755 modules/web/src/router/index.ts create mode 100755 modules/web/src/router/sourceRouter.ts create mode 100755 modules/web/src/source.d.ts create mode 100755 modules/web/src/store/bookStore.ts create mode 100755 modules/web/src/store/connectionStore.ts create mode 100755 modules/web/src/store/index.ts create mode 100755 modules/web/src/store/sourceStore.ts create mode 100755 modules/web/src/utils/souce.ts create mode 100755 modules/web/src/utils/utils.ts create mode 100755 modules/web/src/views/BookChapter.vue create mode 100755 modules/web/src/views/BookShelf.vue create mode 100755 modules/web/src/views/SourceEditor.vue create mode 100755 modules/web/src/web.d.ts create mode 100755 modules/web/tsconfig.app.json create mode 100755 modules/web/tsconfig.json create mode 100755 modules/web/tsconfig.node.json create mode 100755 modules/web/vite.config.ts create mode 100755 package.json create mode 100755 settings.gradle diff --git a/.github/ISSUE_TEMPLATE/01-bugReport.yml b/.github/ISSUE_TEMPLATE/01-bugReport.yml new file mode 100755 index 000000000..c30aa1c5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bugReport.yml @@ -0,0 +1,65 @@ +name: BUG提交 / BUG Report +description: Report bugs to developers +labels: ["BUG"] +body: + - type: checkboxes + attributes: + label: 确认 / Assignments + description: 提交issue请确保完成以下前提,否则该issue可能被忽略 / Make sure you read checkboxs below + options: + - label: 搜索现有issues,不存在相似或相关的issue / No similar or related issues + required: true + - label: 最新[测试版](https://github.com/gedoor/legado/releases/beta)依然存在此问题 / Latest beta app does not work + required: true + - label: 此问题和Xposed、Lsposed、Magisk、手机主题、浏览器插件、无障碍服务等无关 / Make sure your machine is not touched by hook frameworks, plugins, accessibility etc + required: true + + - type: textarea + attributes: + label: 问题描述 / Describe Bugs + validations: + required: true + - type: textarea + attributes: + label: 复现步骤 / How to reproduce + validations: + required: true + - type: checkboxes + attributes: + label: 确认 / Assignment + options: + - label: 已经提交复现所需要的附加资料 / Submit additions related with bugs + required: true + - type: textarea + attributes: + label: 其他信息 / Additions + description: | + 反馈WEB书架前端问题时请提供浏览器版本信息,如Edge 129.0.2792.89 + placeholder: "请用```将提交的内容包裹" + + - type: textarea + attributes: + label: 日志提交 / Relevant log output + description: | + 阅读日志位于我的-关于-崩溃日志、保存日志、书架-右上角-日志,或者自行使用log工具抓取日志 + 如果崩溃日志中包含`java.lang.OutOfMemoryError`,请安装最新测试版,在其他设置里打开记录堆转储,复现崩溃后去关于那里点保存日志,然后去备份目录里将heapDump文件夹里的文件打包压缩一下上传上来 + placeholder: "请用```将日志内容包裹" + + - type: input + attributes: + label: 阅读版本 / Legado version + placeholder: "3.22.110823" + validations: + required: true + - type: input + attributes: + label: Android版本 / Android version + placeholder: "Android 12" + validations: + required: true + - type: input + attributes: + label: 机型 / Model + placeholder: "Redmi K30 Pro" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/02-featureRequest.yml b/.github/ISSUE_TEMPLATE/02-featureRequest.yml new file mode 100755 index 000000000..163619682 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-featureRequest.yml @@ -0,0 +1,40 @@ +name: 功能请求 / Features +description: Request new features +labels: ["需求"] +body: + - type: checkboxes + attributes: + label: 确认 / Assignments + description: 提交issue请确保完成以下前提,否则该issue可能被忽略 / Make sure you read checkbox below + options: + - label: 搜索现有issues,不存在相似或相关的issue / No related requests + required: true + - label: 最新[测试版](https://github.com/gedoor/legado/releases/beta)没有此功能 / Latest beta app does not this feature + required: true + + - type: textarea + attributes: + label: 功能描述 / Features + placeholder: 请清晰的、详细的描述你想要的功能 + validations: + required: true + - type: textarea + attributes: + label: 期望实现方式 / How to implement + placeholder: 阅读应该如何实现该功能 + validations: + required: true + - type: input + attributes: + label: 阅读版本 / Legado version + placeholder: "3.22.110823" + validations: + required: true + - type: textarea + attributes: + label: 附加信息 / Additions + placeholder: 其他的与功能相关的附加信息 + - type: textarea + attributes: + label: 效果演示 / Demo + placeholder: 可以手绘一些草图,或者提供可借鉴的图片 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100755 index 000000000..ee48d87b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: 简繁转化 + url: https://github.com/liuyueyi/quick-chinese-transfer/issues/new + about: 简繁转化问题请优先到quick-chinese-transfer反馈 + - name: 讨论 / Discussions + url: https://github.com/gedoor/legado/discussions + about: Please ask and answer questions here. + - name: 常见问题 / Wiki + url: https://github.com/gedoor/legado/wiki + about: Read wiki if your are new here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100755 index 000000000..64bd1ff5e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +registries: + maven-google: + type: maven-repository + # url: https://maven.google.com + url: https://dl.google.com/dl/android/maven2/ + password: dummy + username: dummy + maven-central: + type: maven-repository + url: https://repo1.maven.org/maven2/ + password: dummy + username: dummy + maven-jitpack: + type: maven-repository + url: https://jitpack.io + password: dummy + username: dummy + +updates: + - package-ecosystem: gradle + directory: "/" + schedule: + interval: "weekly" + registries: "*" + open-pull-requests-limit: 20 + groups: + kotlin_KSP: + patterns: + - "org.jetbrains.kotlin:*" + - "com.google.devtools.ksp" + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: npm + directory: "modules/web" + schedule: + interval: "weekly" diff --git a/.github/scripts/cronet.sh b/.github/scripts/cronet.sh new file mode 100755 index 000000000..a9f293d74 --- /dev/null +++ b/.github/scripts/cronet.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +#分支Stable Dev Beta +branch=$1 +#api 最大偏移 +max_offset=$2 + +[ -z $1 ] && branch=Stable +[ -z $2 ] && max_offset=3 +[ -z $GITHUB_ENV ] && echo "Error: Unexpected github workflow environment" && exit + +offset=0 + +function fetch_version() { + # 获取最新cronet版本 + lastest_cronet_version=`curl -s "https://chromiumdash.appspot.com/fetch_releases?channel=$branch&platform=Android&num=1&offset=$offset" | jq .[0].version -r` + echo "lastest_cronet_version: $lastest_cronet_version" + #lastest_cronet_version=100.0.4845.0 + lastest_cronet_main_version=${lastest_cronet_version%%\.*}.0.0.0 + check_version_exit +} +function check_version_exit() { + # 检查版本是否存在 + local jar_url="https://storage.googleapis.com/chromium-cronet/android/$lastest_cronet_version/Release/cronet/cronet_api.jar" + statusCode=$(curl -s -I -w %{http_code} "$jar_url" -o /dev/null) + if [ $statusCode == "404" ]; then + echo "storage.googleapis.com return 404 for cronet $lastest_cronet_version" + if [[ $max_offset > $offset ]]; then + offset=$(expr $offset + 1) + echo "retry with offset $offset" + fetch_version + else + exit + fi + fi +} +function version_compare() { + # 版本号比较 本地版本小于远程版本时返回0 + local local_version=$1 + local remote_version=$2 + if [[ $local_version == $remote_version ]]; then + return 1 + fi + if [[ $(printf '%s\n' "$1" "$2" | sort -V | head -n1) == $remote_version ]]; then + return 1 + else + return 0 + fi +} + +# 添加变量到github env +function write_github_env_variable() { + echo "$1=$2" >> $GITHUB_ENV +} + +function sync_proguard_rules() { + local raw_github_git="https://raw.githubusercontent.com/chromium/chromium/$lastest_cronet_version" + local proguard_paths=( + components/cronet/android/cronet_combined_impl_native_proguard_golden.cfg + ) + local proguard_rules_path="$GITHUB_WORKSPACE/app/cronet-proguard-rules.pro" + rm -f $proguard_rules_path + echo "fetch cronet proguard rules from upstream $raw_github_git" + for path in ${proguard_paths[@]} + do + echo "fetching $path ..." + curl "$raw_github_git/$path" >> $proguard_rules_path + done +} +########## +# 获取本地cronet版本 +path=$GITHUB_WORKSPACE/gradle.properties +current_cronet_version=`cat $path | grep CronetVersion | sed s/CronetVersion=//` +echo "current_cronet_version: $current_cronet_version" + +echo "fetch $branch release info from https://chromiumdash.appspot.com ..." +fetch_version + +if version_compare $current_cronet_version $lastest_cronet_version; then + # 更新gradle.properties + sed -i s/CronetVersion=.*/CronetVersion=$lastest_cronet_version/ $path + sed -i s/CronetMainVersion=.*/CronetMainVersion=$lastest_cronet_main_version/ $path + # 更新cronet_proguard_rules.pro + sync_proguard_rules + # 更新cronet版本 + sed -i "s/## cronet版本: .*/## cronet版本: $lastest_cronet_version/" $GITHUB_WORKSPACE/app/src/main/assets/updateLog.md + # 生成pull request信息 + write_github_env_variable PR_TITLE "Bump cronet from $current_cronet_version to $lastest_cronet_version" + write_github_env_variable PR_BODY "Changes in the [Git log](https://chromium.googlesource.com/chromium/src/+log/$current_cronet_version..$lastest_cronet_version)" + # 生成cronet flag + write_github_env_variable cronet ok +fi diff --git a/.github/scripts/lzy_web.py b/.github/scripts/lzy_web.py new file mode 100755 index 000000000..f39d4b172 --- /dev/null +++ b/.github/scripts/lzy_web.py @@ -0,0 +1,98 @@ +import requests, os, datetime, sys + +# Cookie 中 phpdisk_info 的值 +cookie_phpdisk_info = os.environ.get('phpdisk_info') +# Cookie 中 ylogin 的值 +cookie_ylogin = os.environ.get('ylogin') + +# 请求头 +headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45', + 'Accept-Language': 'zh-CN,zh;q=0.9', + 'Referer': 'https://pc.woozooo.com/account.php?action=login' +} + +# 小饼干 +cookie = { + 'ylogin': cookie_ylogin, + 'phpdisk_info': cookie_phpdisk_info +} + + +# 日志打印 +def log(msg): + utc_time = datetime.datetime.utcnow() + china_time = utc_time + datetime.timedelta(hours=8) + print(f"[{china_time.strftime('%Y.%m.%d %H:%M:%S')}] {msg}") + + +# 检查是否已登录 +def login_by_cookie(): + url_account = "https://pc.woozooo.com/account.php" + if cookie['phpdisk_info'] is None: + log('ERROR: 请指定 Cookie 中 phpdisk_info 的值!') + return False + if cookie['ylogin'] is None: + log('ERROR: 请指定 Cookie 中 ylogin 的值!') + return False + res = requests.get(url_account, headers=headers, cookies=cookie, verify=True) + if '网盘用户登录' in res.text: + log('ERROR: 登录失败,请更新Cookie') + return False + else: + log('登录成功') + return True + + +# 上传文件 +def upload_file(file_dir, folder_id): + file_name = os.path.basename(file_dir) + url_upload = "https://up.woozooo.com/fileup.php" + headers['Referer'] = f'https://up.woozooo.com/mydisk.php?item=files&action=index&u={cookie_ylogin}' + post_data = { + "task": "1", + "folder_id": folder_id, + "id": "WU_FILE_0", + "name": file_name, + } + files = {'upload_file': (file_name, open(file_dir, "rb"), 'application/octet-stream')} + res = requests.post(url_upload, data=post_data, files=files, headers=headers, cookies=cookie, timeout=120).json() + log(f"{file_dir} -> {res['info']}") + return res['zt'] == 1 + + +# 上传文件夹内的文件 +def upload_folder(folder_dir, folder_id): + file_list = sorted(os.listdir(folder_dir), reverse=True) + for file in file_list: + path = os.path.join(folder_dir, file) + if os.path.isfile(path): + upload_file(path, folder_id) + else: + upload_folder(path, folder_id) + + +# 上传 +def upload(dir, folder_id): + if dir is None: + log('ERROR: 请指定上传的文件路径') + return + if folder_id is None: + log('ERROR: 请指定蓝奏云的文件夹id') + return + if os.path.isfile(dir): + upload_file(dir, str(folder_id)) + else: + upload_folder(dir, str(folder_id)) + + +if __name__ == '__main__': + argv = sys.argv[1:] + if len(argv) != 2: + log('ERROR: 参数错误,请以这种格式重新尝试\npython lzy_web.py 需上传的路径 蓝奏云文件夹id') + # 需上传的路径 + upload_path = argv[0] + # 蓝奏云文件夹id + lzy_folder_id = argv[1] + if login_by_cookie(): + upload(upload_path, lzy_folder_id) diff --git a/.github/scripts/tg_bot.py b/.github/scripts/tg_bot.py new file mode 100755 index 000000000..88a3a8bb5 --- /dev/null +++ b/.github/scripts/tg_bot.py @@ -0,0 +1,47 @@ +import os, sys, telebot + +# 上传文件 +def upload_file(tb, chat_id, file_dir): + doc = open(file_dir, 'rb') + tb.send_document(chat_id, doc) + +# 上传文件夹内的文件 +def upload_folder(tb, chat_id, folder_dir): + file_list = sorted(os.listdir(folder_dir)) + for file in file_list: + path = os.path.join(folder_dir, file) + if os.path.isfile(path): + upload_file(tb, chat_id, path) + else: + upload_folder(tb, chat_id, path) + +# 上传 +def upload(tb, chat_id, dir): + if tb is None: + log('ERROR: 输入正确的token') + return + if chat_id is None: + log('ERROR: 输入正确的chat_id') + return + if dir is None: + log('ERROR: 请指定上传的文件路径') + return + if os.path.isfile(dir): + upload_file(tb, chat_id, dir) + else: + upload_folder(tb, chat_id, dir) + +if __name__ == '__main__': + argv = sys.argv[1:] + if len(argv) != 3: + log('ERROR: 参数错误,请以这种格式重新尝试\npython tg_bot.py $token $chat_id 待上传的路径') + # Token + TOKEN = argv[0] + # chat_id + chat_id = argv[1] + # 待上传文件的路径 + upload_path = argv[2] + #创建连接 + tb = telebot.TeleBot(TOKEN) + #开始上传 + upload(tb, chat_id, upload_path) diff --git a/.github/workflows/autoupdatefork.yml b/.github/workflows/autoupdatefork.yml new file mode 100755 index 000000000..4206fd2b7 --- /dev/null +++ b/.github/workflows/autoupdatefork.yml @@ -0,0 +1,26 @@ +#更新fork +name: update fork + +on: + schedule: + - cron: '0 16 * * *' #0点定时任务 + +jobs: + build: + runs-on: ubuntu-latest + if: ${{ github.event.repository.owner.id == github.event.sender.id && github.actor != 'gedoor' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set env + run: | + git config --global user.email "github-actions@github.com" + git config --global user.name "github-actions" + - name: Update fork + run: | + git remote add upstream https://github.com/gedoor/legado.git + git remote -v + git fetch upstream + git checkout master + git merge upstream/master + git push origin master diff --git a/.github/workflows/cronet.yml b/.github/workflows/cronet.yml new file mode 100755 index 000000000..80ced27be --- /dev/null +++ b/.github/workflows/cronet.yml @@ -0,0 +1,53 @@ +name: Update Cronet + +on: + schedule: + # 周一北京时间9点 + - cron: 0 1 * * 1 + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + if: ${{ github.repository == 'gedoor/legado' }} + steps: + - uses: actions/checkout@v4 + + - name: Check Cronet Updates + run: source .github/scripts/cronet.sh + + - name: Set up JDK 17 + if: ${{ env.cronet == 'ok' }} + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + + - uses: gradle/actions/setup-gradle@v4 + if: ${{ env.cronet == 'ok' }} + + - name: Download Cronet + if: ${{ env.cronet == 'ok' }} + run: | + chmod +x gradlew + ./gradlew app:downloadCronet + + - name: Create Pull Request + if: ${{ env.cronet == 'ok' }} + uses: peter-evans/create-pull-request@v7 + continue-on-error: true + with: + token: ${{ secrets.ACTIONS_TOKEN }} + title: ${{ env.PR_TITLE }} + commit-message: | + ${{ env.PR_TITLE }} + - ${{ env.PR_BODY }} + body: ${{ env.PR_BODY }} + branch: cronet + delete-branch: true + add-paths: | + *cronet*jar + *cronet.json + *updateLog.md + *gradle.properties + *cronet-proguard-rules.pro diff --git a/.github/workflows/legado.jks b/.github/workflows/legado.jks new file mode 100755 index 0000000000000000000000000000000000000000..89ea725fde4c53e4a937051e667fd8f6113769c0 GIT binary patch literal 2232 zcmchY`8U*yAI9f1Ge`_3u8FwzL86br*q2dR`p#Gy%N5O)EoI`OWh{}gL`s<~VXVnc zw#Zno<&LY#5FsJ^E`(6xI``ar&OQAB-yfbIp4U(B^PJ~A@BNki6$k_ZJrwY_ILYo; zF1z_0TK^oJe=Y>V0R^e>eUOjys1hdt2Tnqf0Kfr(Q{jt*J+r#G-DfGmdYWQ2SgGFi zZ-`&Xl}|-1I|VO`-Ban(G&x~_C4HDl&v>|5OD{APt&gHhrWl&Z^WNKDPPghS5Q!Bt6gyh_EZR}4BF~#+`%K|&KbZ?c711}G(+v6?uibUMOh~eM&Rn4N|SvRljLbay*zKJ0J!!-B4-J#J#hfvLoJVz;G|xr;YJj%G;0? z_AEC7GF>i~M~lWZVA>vcvoEKzmUC@nswR^o~aSM@3@}qFkc*(g?%E#A0M{E zmE7O0LSk?y%-Hu9<_|`$(;cJ9BRj~HnW>%Pt9DU2vtR~PJeP$YMwIkFa7E_uhcx7B&y%uI9JpL=qfk+8Z{Byyr#sYbbJ%EdX1 zf`s!*GmACZ8zB~B41QJ5)_6ai6*q{0gv{I{SHT4to6)D)4sN?+D<_T5h)>hHHQ}dVY{`KXdEFv5uYmWq=zPBt~ngUKE<)?*L$J1N3#|!Nf+`7B0}{a zL-fw&kAvRAh7m%-%i2xlnXAM zbx)a!iC)M$vsSv-;X%@(MLSgOwv!nQ^MlEI8&_Od{=K3>S=9{(`8dAjwXH4IVCgL^ zSaV8S{`7#aJ_Db2>c%5plTKr!p=#02_}n0d?W%)G?ML5B%{s)ci7+pm^8dC~k~YOy zZ@DxdHV>VsLJv!*C-sNL>(V4U(wgZgb*wDWzGgXT_3kW6Z4Ul^1qZ$X+bFOsp0~qB zcirTe^SXu=*5`yH^@{V2TI?P_tUCG?k-<7uFa;2Y-dgs)+lL0qqAA+;^3nl11;m}F z>fL*{7IUvYSh78Mtc?AzQ07+`ZYY*~C}_z=g^4JEJM*foy{ z>2LY(WKN!5euui>R;uhVn+>^I`{gRMpBhvgIOw{yC8qC{Y#L})J@#3kUN?n}5^m>v zj(XUAxyj-zWx*w!#-WD6$lS1Nl+jz5t&`jx7bHpI7DfFq{!IJ#Xdu0#%>9tr9M>GFmx@JGT-#$2; zyk=I%;W9swd^I$GfyB9Z$Jk|}PVEJKTrJ83ZIbLx&Ty9t9|&3PNwu5SknvHc-=Ev8 zB5aw*X3RSp+x;O-;%U00&;1U8W9fu+&n6z|5Q-}``JIm(H_kLD^3)xWm2)3E7`D}# z!fmzJu=&W{h3$_w`7(EoxTAvct|jeImzIQpn$a2UR1|r#rGMWLv5@;7`qL*lE(ews zy$X;~Y>v^287u> $GITHUB_OUTPUT + - id: check + run: | + if [ ! -z "${{ secrets.RELEASE_KEY_STORE }}" ]; then + echo "sign=yes" >> $GITHUB_OUTPUT + fi + if [ ! -z "${{ secrets.SERVICE_ACCOUNT_JSON }}" ]; then + echo "play=yes" >> $GITHUB_OUTPUT + fi + + build: + needs: prepare + if: ${{ needs.prepare.outputs.sign }} + strategy: + matrix: + product: [ app, google ] + fail-fast: false + runs-on: ubuntu-latest + env: + product: ${{ matrix.product }} + VERSION: ${{ needs.prepare.outputs.version }} + play: ${{ needs.prepare.outputs.play }} + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + + - name: Release Apk Sign + run: | + # not use this output + # echo "KeyStore=yes" >> $GITHUB_OUTPUT + echo -e "\n" >> gradle.properties + echo RELEASE_KEY_ALIAS='${{ secrets.RELEASE_KEY_ALIAS }}' >> gradle.properties + echo RELEASE_KEY_PASSWORD='${{ secrets.RELEASE_KEY_PASSWORD }}' >> gradle.properties + echo RELEASE_STORE_PASSWORD='${{ secrets.RELEASE_STORE_PASSWORD }}' >> gradle.properties + echo RELEASE_STORE_FILE='./key.jks' >> gradle.properties + echo ${{ secrets.RELEASE_KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks + + - name: Unify Version Name + run: | + echo "统一版本号" + sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build With Gradle + run: | + echo "开始进行${{ env.product }}构建" + chmod +x gradlew + ./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all + + - name: Organize the Files + run: | + mkdir -p ${{ github.workspace }}/apk/ + cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/ + + - name: Upload App To Artifact + uses: actions/upload-artifact@v4 + with: + name: legado_${{ env.product }} + path: ${{ github.workspace }}/apk/*.apk + + - name: Release + if: ${{ env.product == 'app' }} + uses: softprops/action-gh-release@v2 + with: + name: legado_app_${{ env.VERSION }} + tag_name: ${{ env.VERSION }} + body_path: ${{ github.workspace }}/CHANGELOG.md + draft: false + prerelease: false + files: ${{ github.workspace }}/apk/legado_app_*.apk + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare For GooglePlay + if: env.product == 'google' && env.play == 'yes' + run: | + mkdir -p ReleaseNotes + ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-en-US + ln -s ${{ github.workspace }}/CHANGELOG.md ReleaseNotes/whatsnew-zh-CN + + - name: Release To GooglePlay + if: env.product == 'google' && env.play == 'yes' + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} + packageName: io.legado.play + releaseFiles: ${{ github.workspace }}/apk/legado_google_*.apk + track: production + whatsNewDirectory: ${{ github.workspace }}/ReleaseNotes + + - name: Push Assets To "release" Branch + if: ${{ github.actor == 'gedoor' }} + run: | + cd $GITHUB_WORKSPACE/apk/ + git init + git checkout -b release + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release" + git add *.apk + git commit -m "${{ env.VERSION }}" + git push -f -u origin release + + - name: Purge Jsdelivr Cache + if: ${{ github.actor == 'gedoor' }} + run: | + result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.actor }}/release@release/) + if echo $result |grep -q 'success.*true'; then + echo "jsdelivr缓存更新成功" + else + echo $result + fi diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100755 index 000000000..bfa2c0d0c --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,29 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: closeStaleIssue + +on: + schedule: + # 每5天北京时间9点 + - cron: '30 1 1/5 * *' + workflow_dispatch: + +jobs: + stale: + + runs-on: ubuntu-latest + if: ${{ github.repository == 'gedoor/legado' }} + permissions: + issues: write + + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: '由于长期没有状态更新,该问题将于5天后自动关闭。如有需要可重新打开。' + days-before-stale: 30 + days-before-close: 5 + operations-per-run: 100 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100755 index 000000000..2a7fdc83a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,278 @@ +name: Test Build + +on: + push: + branches: + - master + paths: + - '**' + # - '!**/assets/**' + # - '!**.md' + - '!**/ISSUE_TEMPLATE/**' + - '!**/modules/web/**' + pull_request: + paths-ignore: + - '**/modules/web/**' + workflow_run: + workflows: [Build Web] + branches: [master] + types: + - completed + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + prepare: + runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.head_commit.message, 'Merge pull request') }} + outputs: + version: ${{ steps.set-ver.outputs.version }} + versionL: ${{ steps.set-ver.outputs.versionL }} + lanzou: ${{ steps.check.outputs.lanzou }} + telegram: ${{ steps.check.outputs.telegram }} + steps: + - id: set-ver + run: | + echo "version=$(date -d "8 hour" -u +3.%y.%m%d%H)" >> $GITHUB_OUTPUT + echo "versionL=$(date -d "8 hour" -u +3.%y.%m%d%H%M)" >> $GITHUB_OUTPUT + - id: check + run: | + if [ ${{ secrets.LANZOU_ID }} ]; then + echo "lanzou=yes" >> $GITHUB_OUTPUT + fi + if [ ${{ secrets.BOT_TOKEN }} ]; then + echo "telegram=yes" >> $GITHUB_OUTPUT + fi + + build: + needs: prepare + strategy: + matrix: + product: [ app ] + type: [ release, releaseA ] + fail-fast: false + runs-on: ubuntu-latest + env: + product: ${{ matrix.product }} + type: ${{ matrix.type }} + VERSION: ${{ needs.prepare.outputs.version }} + VERSIONL: ${{ needs.prepare.outputs.versionL }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + - name: Clear 18PlusList.txt + run: | + echo "清空18PlusList.txt" + echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt + - name: Release Apk Sign + run: | + echo "给apk增加签名" + cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks + sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i + sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i + sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i + sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build With Gradle + continue-on-error: true + run: | + if [ ${{ env.type }} == 'release' ]; then + typeName="原包名" + else + typeName="共存" + sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i + sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i + fi + echo "统一版本号" + sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i + echo "开始${{ env.product }}$typeName构建" + chmod +x gradlew + ./gradlew assemble${{ env.product }}release --build-cache --parallel --daemon --warning-mode all + echo "修改APK文件名" + mkdir -p ${{ github.workspace }}/apk/ + for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do + mv "$file" ${{ github.workspace }}/apk/legado_${{ env.product }}_${{ env.VERSIONL }}_$typeName.apk + done + echo "移动mapping文件" + mkdir -p ${{ github.workspace }}/mapping/ + for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/mapping.txt`; do + mv "$file" ${{ github.workspace }}/mapping/mapping.txt + done + + - name: Move Missing Rules Files + run: | + echo "移动missing_rules.txt文件" + mkdir -p ${{ github.workspace }}/mapping/ + for file in `ls ${{ github.workspace }}/app/build/outputs/mapping/*/missing_rules.txt`; do + mv "$file" ${{ github.workspace }}/mapping/missing_rules.txt + done + + - name: Upload Missing Rules File To Artifact + uses: actions/upload-artifact@v4 + with: + name: legado.${{ env.product }}.${{ env.type }}.mapping.missing_rules + if-no-files-found: ignore + path: ${{ github.workspace }}/mapping/missing_rules.txt + + - name: Check Build production + run: | + if [ ! -d ${{ github.workspace }}/apk ]; then + echo "Build production not found! Check gradle logs." + exit 1 + fi + cd ${{ github.workspace }}/apk/ + if [ ! -e legado_*.apk ]; then + echo "Build production not found! Check gradle logs." + exit 1 + fi + + - name: Upload App To Artifact + uses: actions/upload-artifact@v4 + with: + name: legado.${{ env.product }}.${{ env.type }} + if-no-files-found: ignore + path: ${{ github.workspace }}/apk/*.apk + + - name: Upload Mapping File To Artifact + uses: actions/upload-artifact@v4 + with: + name: legado.${{ env.product }}.${{ env.type }}.mapping + if-no-files-found: ignore + path: ${{ github.workspace }}/mapping/mapping.txt + + prerelease: + needs: [ prepare, build ] + if: github.event_name != 'pull_request' && github.repository == 'gedoor/legado' + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.prepare.outputs.version }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: apk/ + + - working-directory: apk/ + run: | + mv */*.apk . + rm -rf */ + + for file in `ls *.apk`; do + if [[ "$file" == *原包名* ]]; then + mv "$file" $(echo $file | sed s/原包名/release/) + else + mv "$file" $(echo $file | sed s/共存/releaseA/) + fi + done + + - name: Delete Pre-Release + run: | + if gh release view beta &>/dev/null; then + gh release delete beta -y + fi + env: + GH_TOKEN: ${{ github.token }} + + - name: Create or update beta tag + uses: richardsimko/update-tag@v1 + with: + tag_name: beta + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish Pre-Release + uses: ncipollo/release-action@v1 + with: + name: legado_app_${{ env.VERSION }} + tag: "beta" + body: | + 此版本为测试版,签名与正式版不同,可能存在不稳定情况,升级前请务必备份好数据。 + releaseA 为共存版本,可同时安装使用,功能没有区别。 + prerelease: true + artifacts: ${{ github.workspace }}/apk/*.apk + + lanzou: + needs: [ prepare, build ] + if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.lanzou == 'yes' }} + runs-on: ubuntu-latest + env: + # 登录蓝奏云后在控制台运行document.cookie + ylogin: ${{ secrets.LANZOU_ID }} + phpdisk_info: ${{ secrets.LANZOU_PSD }} + # 蓝奏云里的文件夹ID(阅读3测试版:2670621) + LANZOU_FOLDER_ID: ${{ secrets.LANZOU_FOLDER_ID }} + #蓝奏云分享链接 + LANZOU_URL: ${{ secrets.LANZOU_URL }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: apk/ + - working-directory: apk/ + run: mv */*.apk . ;rm -rf */ + - name: Upload To Lanzou + continue-on-error: true + run: | + path="$GITHUB_WORKSPACE/apk/" + python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID" + echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: $LANZOU_URL" + + test_Branch: + needs: [ prepare, build ] + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' && github.actor == 'gedoor' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: apk/ + - working-directory: apk/ + run: mv */*.apk . ;rm -rf */ + - name: Push To "test" Branch + run: | + cd $GITHUB_WORKSPACE/apk/ + git init + git checkout -b test + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote add origin "https://${{ github.actor }}:${{ secrets.ACTIONS_TOKEN }}@github.com/${{ github.actor }}/release" + git add *.apk + git commit -m "${{ needs.prepare.outputs.versionL }}" + git push -f -u origin test + + telegram: + needs: [ prepare, build ] + if: ${{ github.event_name != 'pull_request' && needs.prepare.outputs.telegram == 'yes' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + path: apk/ + - working-directory: apk/ + run: | + for file in `ls */*.apk`; do + mv "$file" "$(echo "$file"|sed -e 's#.*\/##g' -e "s/_/ /g" -e 's/legado/阅读/')" + done + rm -rf */ + - name: Post to channel + uses: xireiki/channel-post@v1 + with: + chat_id: ${{ secrets.CHANNEL_ID }} + bot_token: ${{ secrets.BOT_TOKEN }} + context: "#阅读 #Legado #Beta ${{ needs.prepare.outputs.versionL }}" + path: apk/* + method: sendFile diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml new file mode 100755 index 000000000..8a15e1eee --- /dev/null +++ b/.github/workflows/web.yml @@ -0,0 +1,65 @@ +name: Build Web + +on: + push: + branches: + - master + paths: + - '**/modules/web/**' + pull_request: + paths: + - '**/modules/web/**' + workflow_dispatch: + +env: + UPSTREAM_REPOSITORY: gedoor/legado + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + id: pnpm-install + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/web/package.json') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Build and move files + working-directory: modules/web + run: | + pnpm i + pnpm build + version="v$(date -d "8 hour" -u +3.%y.%m%d%H)" + echo "APP_VER=$version" >> $GITHUB_ENV + + - name: push changes + if: ${{ github.event_name != 'pull_request' && github.repository == env.UPSTREAM_REPOSITORY }} + uses: stefanzweifel/git-auto-commit-action@v5.1.0 + with: + commit_message: Bump web ${{ env.APP_VER}} + file_pattern: app/src/main/assets/web/vue/ + diff --git a/.gitignore b/.gitignore new file mode 100755 index 000000000..d6401dcba --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.iml +.gradle +local.properties +.DS_Store +/build +build/ +/captures +.externalNativeBuild +/release +/tmp +node_modules/ +/app/app +/app/google +/app/gradle.properties +package-lock.json +.idea/ +# Kotlin 2.0 +.kotlin/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 000000000..1e3d601a0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +**2022/10/02** + +* 更新cronet: 106.0.5249.79 +* 正文选择菜单朗读按钮长按可切换朗读选择内容和从选择开始处一直朗读 +* 源编辑输入框设置最大行数12,在行数特别多的时候更容易滚动到其它输入 +* 修复某些情况下无法搜索到标题的bug,净化规则较多的可能会降低搜索速度 by Xwite +* 修复文件类书源换源后阅读bug by Xwite +* Cronet 支持DnsHttpsSvcb by g2s20150909 +* 修复web进度同步问题 by 821938089 +* 启用混淆以减小app大小 有bug请带日志反馈 +* 其它一些优化 diff --git a/English.md b/English.md new file mode 100755 index 000000000..9cc6b9800 --- /dev/null +++ b/English.md @@ -0,0 +1,149 @@ +# [English](English.md) [中文](README.md) + +[![icon_android](https://github.com/gedoor/gedoor.github.io/blob/master/static/img/legado/icon_android.png)](https://play.google.com/store/apps/details?id=io.legado.play.release) + +idea + +
+legado + +Legado / 开源阅读 +
+gedoor.github.io / legado.top +
+Legado is a free and open source novel reader for Android. +
+ +[![](https://img.shields.io/badge/-Contents:-696969.svg)](#contents) [![](https://img.shields.io/badge/-Function-F5F5F5.svg)](#Function-) [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-) [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-) [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-) [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-) [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-) [![](https://img.shields.io/badge/-Interface-F5F5F5.svg)](#Interface-) + +>New user? +> +>The software does not provide content, you need to add it manually, such as importing book sources, etc. +>Take a look at [official help documentation](https://www.yuque.com/legado/wiki),Maybe there's an answer you need inside. + +# Function [![](https://img.shields.io/badge/-Function-F5F5F5.svg)](#Function-) + +You can customize the book source, set your own rules, and capture web page data. The rules are simple and easy to understand. There are rules in the software. List bookshelf, grid bookshelf switch freely. The book source rules support search and discovery, and all the functions of finding books and reading books are all customized, making it easier to find books. +* Custom ebook sources, set your own rules to capture web data, the rules are simple and easy to understand, the software has a rule description. +* eBook sources rules support search and discovery, all find books and read books function all custom, find books more convenient. +* Schedule updating your library for new chapters. +* Online reading from web sources that can be imported in bulk +* Local reading of Auto-download episodes. +* Local reading of TXT or EPUB files +* ebook Wishlist +* Big text viewer. You can open eBook and txt in 1GB size +* Automatic text replacement for removing ad in content +* List bookshelf, grid bookshelf free to switch. +* Subscription content, you can subscribe to any content you want to see, see what you want to see +* A configurable reader with fonts, background, page transitions mode and other settings +* Timer. Set interval time to listen ebook, time up, ebook turn off completely. +* TTS book reader. tts can optionally be install“smartvoice-4.1.0” or ”Speech Services by Google“ Give your baby a storybook to listen to and teach your baby to talk, +* Dark mode and E-Ink mode support and Web service support +* Create backups to local or WebDav server +* Decentralization web3 +* Support replacement purification, it is very convenient to remove the content of advertisement replacement. +* Support local TXT, EPUB reading, manual browsing, intelligent scanning. +* Support highly customized reading interface, switch font, color, background, line spacing, paragraph spacing, bold, simplified and traditional conversion. +* Support multiple page turning modes, covering, emulating, sliding, scrolling, etc. + + + + # + + +# Download [![](https://img.shields.io/badge/-Download-F5F5F5.svg)](#Download-) + +#### Android + +* [Releases](https://github.com/gedoor/legado/releases/latest) +* [Google play - $1.99](https://play.google.com/store/apps/details?id=io.legado.play.release) +* [Coolapk](https://www.coolapk.com/apk/io.legado.app.release) +* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b) +* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release) + + +#### IOS + +* Stopped(No release) - [Github](https://github.com/gedoor/YueDuFlutter) + + + # + + +# Community [![](https://img.shields.io/badge/-Community-F5F5F5.svg)](#Community-) + +#### Telegram + +[![Telegram-group](https://img.shields.io/badge/Telegram-group-blue)](https://t.me/yueduguanfang) [![Telegram-channel](https://img.shields.io/badge/Telegram-channel-blue)](https://t.me/legado_channels) + +#### Discord + +[![Discord](https://img.shields.io/discord/560731361414086666?color=%235865f2&label=Discord)](https://discord.gg/VtUfRyzRXn) + +#### Other + +https://www.yuque.com/legado/wiki/community + + + # + + +# API [![](https://img.shields.io/badge/-API-F5F5F5.svg)](#API-) + +* Legado 3.0 The API is provided in 2 ways: `Web way` and `Content Provider way`. You can call it yourself as needed in [here](api.md). +* One-click import by url recall reading, url format: legado://import/{path}?src={url} +* Path Type: bookSource,rssSource,replaceRule,textTocRule,httpTTS,theme,readConfig,dictRule,addToBookshelf +* path type explanation: Book source, subscription source, replacement rules, local txt novel directory rules, online reading engine, theme, reading layout, [add to bookshelf](/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt) + + + # + + +# Other [![](https://img.shields.io/badge/-Other-F5F5F5.svg)](#Other-) + +##### Disclaimers + +https://gedoor.github.io/Disclaimer + +##### Legado 3.0 + +* [eBook sources rules](https://mgz0227.github.io/The-tutorial-of-Legado/) +* [Update Log](/app/src/main/assets/updateLog.md) +* [Help Documentation](/app/src/main/assets/web/help/md/appHelp.md) +* [web bookshelf](https://github.com/gedoor/legado_web_bookshelf) +* [web source editor](https://github.com/gedoor/legado_web_source_editor) + + + # + + +# Grateful [![](https://img.shields.io/badge/-Grateful-F5F5F5.svg)](#Grateful-) + +> * org.jsoup:jsoup +> * cn.wanghaomiao:JsoupXpath +> * com.jayway.jsonpath:json-path +> * com.github.gedoor:rhino-android +> * com.squareup.okhttp3:okhttp +> * com.github.bumptech.glide:glide +> * org.nanohttpd:nanohttpd +> * org.nanohttpd:nanohttpd-websocket +> * cn.bingoogolapple:bga-qrcode-zxing +> * com.jaredrummler:colorpicker +> * org.apache.commons:commons-text +> * io.noties.markwon:core +> * io.noties.markwon:image-glide +> * com.hankcs:hanlp +> * com.positiondev.epublib:epublib-core + + + # + + +# Interface [![](https://img.shields.io/badge/-Interface-F5F5F5.svg)](#Interface-) + + + + + + # + diff --git a/LICENSE b/LICENSE new file mode 100755 index 000000000..ef08d94b4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Legado Copyright (C) 2019-2023 gedoor + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/api.md b/api.md new file mode 100755 index 000000000..1ea46b12a --- /dev/null +++ b/api.md @@ -0,0 +1,293 @@ +# 阅读[API](/app/src/main/java/io/legado/app/api/controller) + +## 对于[Web](/app/src/main/java/io/legado/app/web/)的配置 + +您需要先在设置中启用"Web 服务"。 + +## 使用 + +### Web + +以下说明假设您的操作在本机进行,且开放端口为1234。 +如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。 + +#### 插入单个书源 + +请求BODY内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookSource.kt) + +``` +URL = http://127.0.0.1:1234/saveBookSource +Method = POST +``` + +#### 插入多个书源or订阅源 + +请求BODY内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。 + +``` +URL = http://127.0.0.1:1234/saveBookSources +URL = http://127.0.0.1:1234/saveRssSources +Method = POST +``` + +#### 获取书源 + +``` +URL = http://127.0.0.1:1234/getBookSource?url=xxx +URL = http://127.0.0.1:1234/getRssSource?url=xxx +Method = GET +``` + +#### 获取所有书源or订阅源 + +``` +URL = http://127.0.0.1:1234/getBookSources +URL = http://127.0.0.1:1234/getRssSources +Method = GET +``` + +#### 删除多个书源or订阅源 + +请求BODY内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。 + +``` +URL = http://127.0.0.1:1234/deleteBookSources +URL = http://127.0.0.1:1234/deleteRssSources +Method = POST +``` + +#### 调试源 + +key为书源搜索关键词,tag为源链接 + +``` +URL = ws://127.0.0.1:1235/bookSourceDebug +URL = ws://127.0.0.1:1235/rssSourceDebug +Message = { key: [String], tag: [String] } +``` + +#### 获取替换规则 + +``` +URL = http://127.0.0.1:1234/getReplaceRules +Method = GET +``` + +#### 替换规则管理 + +请求BODY内容为`JSON`字符串, +替换规则参考[这个文件](/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt)。 + +##### 删除 + +``` +URL = http://127.0.0.1:1234/deleteReplaceRule +Method = POST +Body = [ReplaceRule] +``` +##### 插入 + +``` +URL = http://127.0.0.1:1234/saveReplaceRule +Method = POST +Body = [ReplaceRule] +``` + +##### 测试 + +返回测试文本text替换结果 + +``` +URL = http://127.0.0.1:1234/testReplaceRule +Method = POST +Body = { rule: [ReplaceRule], text: [String] } +``` + +#### 搜索在线书籍 + +若想获取对应的书籍的目录正文 请先**插入书籍**以启用缓存,如果试读后决定不添加到书籍,请**删除书籍** + +``` +URL = ws://127.0.0.1:1235/searchBook +Message = { key: [String] } +``` + +#### 插入书籍 + +请求BODY内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/Book.kt)。 + +``` +URL = http://127.0.0.1:1234/saveBook +Method = POST +``` + +#### 删除书籍 + +``` +URL = http://127.0.0.1:1234/deleteBook +Method = POST +``` + +#### 获取所有书籍 + +``` +URL = http://127.0.0.1:1234/getBookshelf +Method = GET +``` + +获取APP内的所有书籍。 + +#### 获取书籍章节列表 + +``` +URL = http://127.0.0.1:1234/getChapterList?url=xxx +Method = GET +``` + +获取指定图书的章节列表。 + +#### 获取书籍内容 + +``` +URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1 +Method = GET +``` + +获取指定图书的第`index`章节的文本内容。 + +#### 获取封面 + +``` +URL = http://127.0.0.1:1234/cover?path=xxxxx +Method = GET +``` + +#### 获取正文图片 + +``` +URL = http://127.0.0.1:1234/image?url=${bookUrl}&path=${picUrl}&width=${width} +Method = GET +``` + +#### 保存书籍进度 + +请求BODY内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookProgress.kt)。 + +``` +URL = http://127.0.0.1:1234/saveBookProgress +Method = POST +``` + +### [Content Provider](/app/src/main/java/io/legado/app/api/ReaderProvider.kt) + + +* 需声明`io.legado.READ_WRITE`权限 +* `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败 +* 以下出现的`providerHost`请自行替换 + +#### 插入单个书源or订阅源 + +创建`Key="json"`的`ContentValues`,内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookSource.kt) + +``` +URL = content://providerHost/bookSource/insert +URL = content://providerHost/rssSource/insert +Method = insert +``` + +#### 插入多个书源or订阅源 + +创建`Key="json"`的`ContentValues`,内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。 + +``` +URL = content://providerHost/bookSources/insert +URL = content://providerHost/rssSources/insert +Method = insert +``` + +#### 获取书源or订阅源 + +获取指定URL对应的书源信息。 +用`Cursor.getString(0)`取出返回结果。 + +``` +URL = content://providerHost/bookSource/query?url=xxx +URL = content://providerHost/rssSource/query?url=xxx +Method = query +``` + +#### 获取所有书源or订阅源 + +获取APP内的所有订阅源。 +用`Cursor.getString(0)`取出返回结果。 + +``` +URL = content://providerHost/bookSources/query +URL = content://providerHost/rssSources/query +Method = query +``` + +#### 删除多个书源or订阅源 + +创建`Key="json"`的`ContentValues`,内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。 + +``` +URL = content://providerHost/bookSources/delete +URL = content://providerHost/rssSources/delete +Method = delete +``` + +#### 插入书籍 + +创建`Key="json"`的`ContentValues`,内容为`JSON`字符串, +格式参考[这个文件](/app/src/main/java/io/legado/app/data/entities/Book.kt)。 + +``` +URL = content://providerHost/book/insert +Method = insert +``` + +#### 获取所有书籍 + +获取APP内的所有书籍。 +用`Cursor.getString(0)`取出返回结果。 + +``` +URL = content://providerHost/books/query +Method = query +``` + +#### 获取书籍章节列表 + +获取指定图书的章节列表。 +用`Cursor.getString(0)`取出返回结果。 + +``` +URL = content://providerHost/book/chapter/query?url=xxx +Method = query +``` + +#### 获取书籍内容 + +获取指定图书的第`index`章节的文本内容。 +用`Cursor.getString(0)`取出返回结果。 + +``` +URL = content://providerHost/book/content/query?url=xxx&index=1 +Method = query +``` + +#### 获取封面 + +``` +URL = content://providerHost/book/cover/query?path=xxxx +Method = query +``` diff --git a/app/.gitignore b/app/.gitignore new file mode 100755 index 000000000..e431c26b5 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build +/so \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100755 index 000000000..976fbdfe3 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,292 @@ +plugins { +// id "com.android.application" +// id 'org.jetbrains.kotlin.android' +// id 'kotlin-parcelize' +// //id 'kotlin-kapt' +// id 'com.google.devtools.ksp' +// id "com.google.gms.google-services" + + alias libs.plugins.android.application + alias libs.plugins.kotlin.android + alias libs.plugins.kotlin.parcelize + alias libs.plugins.room + alias libs.plugins.ksp + alias libs.plugins.google.services +} +apply from: 'download.gradle' + +static def releaseTime() { + return new Date().format("yy.MMddHH", TimeZone.getTimeZone("GMT+8")) +} + +def name = "legado" +def version = "3." + releaseTime() +def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim()) + +android { + compileSdk = compile_sdk_version + namespace = 'io.legado.app' + kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + + signingConfigs { + if (project.hasProperty("RELEASE_STORE_FILE")) { + myConfig { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + enableV1Signing = true + enableV2Signing = true + enableV3Signing = true + enableV4Signing = true + } + } + } + defaultConfig { + applicationId "io.legado.app" + minSdk 21 + targetSdk 36 + versionCode 10000 + gitCommits + versionName version + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + project.ext.set("archivesBaseName", name + "_" + version) + + buildConfigField "String", "Cronet_Version", "\"$CronetVersion\"" + buildConfigField "String", "Cronet_Main_Version", "\"$CronetMainVersion\"" + + javaCompileOptions { + annotationProcessorOptions { + arguments += [ + "room.incremental" : "true", + "room.expandProjection": "true", + "room.schemaLocation" : "$projectDir/schemas".toString() + ] + } + } + } + buildFeatures { + buildConfig = true + viewBinding = true + } + buildTypes { + release { + if (project.hasProperty("RELEASE_STORE_FILE")) { + signingConfig signingConfigs.myConfig + } + applicationIdSuffix '.release' + if (getApplicationIdSuffix() == '.releaseA') { + manifestPlaceholders.put("app_name", "@string/app_name_a") + } else { + manifestPlaceholders.put("app_name", "@string/app_name") + } + + minifyEnabled true + shrinkResources = true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'cronet-proguard-rules.pro' + } + debug { + if (project.hasProperty("RELEASE_STORE_FILE")) { + signingConfig signingConfigs.myConfig + } + manifestPlaceholders.put("app_name", "@string/app_name") + + applicationIdSuffix '.debug' + versionNameSuffix 'debug' + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro', 'cronet-proguard-rules.pro' + } + } + //noinspection GrDeprecatedAPIUsage + flavorDimensions = ['mode'] + productFlavors { + app { + dimension "mode" + manifestPlaceholders.put("APP_CHANNEL_VALUE", "app") + } + } + + android.applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + def flavor = variant.productFlavors[0].name + outputFileName = "${name}_${flavor}_${defaultConfig.versionName}.apk" + } + } + + + room { + schemaDirectory "$projectDir/schemas" + } + // 设定Room的KSP参数 + ksp { + arg("room.incremental", "true") + arg("room.expandProjection", "true") + arg("room.generateKotlin", "false") + //arg("room.schemaLocation", "$projectDir/schemas") + + } + + compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled = true + // Sets Java compatibility + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + packaging { + resources.excludes.add('META-INF/*') + } + + sourceSets { + // Adds exported schema location as test app assets. + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + lint { + checkDependencies = true + } + tasks.withType(JavaCompile).tap { + configureEach { + //options.compilerArgs << "-Xlint:unchecked" + } + } +} + +dependencies { + //compileOnly "com.android.tools.build:gradle:$agp_version" + //noinspection GradleDependency,GradlePackageUpdate + //coreLibraryDesugaring('com.android.tools:desugar_jdk_libs_nio:2.0.4') + coreLibraryDesugaring(libs.desugar) + testImplementation(libs.junit) + androidTestImplementation(libs.bundles.androidTest) + //kotlin + //noinspection GradleDependency,DifferentStdlibGradleVersion + implementation(libs.kotlin.stdlib) + //Kotlin反射 + //noinspection GradleDependency,DifferentStdlibGradleVersion + //implementation(libs.kotlin.reflect) + + + //协程 + //def coroutines_version = '1.7.3' + implementation(libs.bundles.coroutines) + + + //图像处理库Toolkit + implementation(libs.renderscript.intrinsics.replacement.toolkit) + + //androidX + implementation(libs.core.ktx) + implementation(libs.appcompat.appcompat) + implementation(libs.activity.ktx) + implementation(libs.fragment.ktx) + implementation(libs.preference.ktx) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.swiperefreshlayout) + implementation(libs.androidx.recyclerview) + implementation(libs.androidx.viewpager2) + implementation(libs.androidx.webkit) + implementation(libs.androidx.documentfile) + + //google + implementation(libs.material) + implementation(libs.flexbox) + implementation(libs.gson) + + //lifecycle + implementation(libs.lifecycle.common.java8) + implementation(libs.lifecycle.service) + + //media + implementation(libs.media.media) + // For media playback using ExoPlayer + implementation(libs.media3.exoplayer) + // For loading data using the OkHttp network stack + implementation(libs.media3.datasource.okhttp) + // For exposing and controlling media sessions + //implementation "androidx.media3:media3-session:$media3_version" + + //Splitties + implementation(libs.splitties.appctx) + implementation(libs.splitties.systemservices) + implementation(libs.splitties.views) + + //room sql语句不高亮解决方法https://issuetracker.google.com/issues/234612964#comment6 + implementation(libs.room.runtime) + implementation(libs.room.ktx) + //kapt("androidx.room:room-compiler:$room_version") + ksp(libs.room.compiler) + androidTestImplementation(libs.room.testing) + + //liveEventBus + implementation(libs.liveeventbus) + + //规则相关 + implementation(libs.jsoup) + implementation(libs.json.path) + implementation(libs.jsoupxpath) + implementation(project(path: ':modules:book')) + implementation(project(path: ':modules:rhino')) + + //JS rhino + + //网络 + implementation(libs.okhttp) + implementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar'])) + implementation(libs.protobuf.javalite) + + //Glide + implementation(libs.glide.glide) + implementation(libs.glide.okhttp) + ksp(libs.glide.ksp) + + //Svg + implementation(libs.androidsvg) + //Glide svg plugin + implementation(libs.glide.svg) + + //webServer + implementation(libs.nanohttpd.nanohttpd) + implementation(libs.nanohttpd.websocket) + + //二维码 + //noinspection GradleDependency + implementation(libs.zxing.lite) + + //颜色选择 + implementation(libs.colorpicker) + + //压缩解压 + implementation libs.libarchive + + //apache + implementation(libs.commons.text) + + //MarkDown + implementation(libs.markwon.core) + implementation(libs.markwon.image.glide) + implementation(libs.markwon.ext.tables) + implementation(libs.markwon.html) + + //转换繁体 + implementation(libs.quick.chinese.transfer.core) + + //加解密类库,有些书源使用 + //noinspection GradleDependency,GradlePackageUpdate + implementation(libs.hutool.crypto) + + //firebase, 崩溃统计和性能统计 + implementation platform(libs.firebase.bom) + implementation libs.firebase.analytics + implementation libs.firebase.perf + + implementation libs.glide.recyclerview + + //LeakCanary, 内存泄露检测 + //debugImplementation('com.squareup.leakcanary:leakcanary-android:2.7') + + //com.github.AmrDeveloper:CodeView代码编辑已集成到应用内 + //epubLib集成到应用内 +} diff --git a/app/cronet-proguard-rules.pro b/app/cronet-proguard-rules.pro new file mode 100755 index 000000000..72042b2dd --- /dev/null +++ b/app/cronet-proguard-rules.pro @@ -0,0 +1,232 @@ +# -------- Config Path: base/android/proguard/shared_with_cronet.flags -------- +# Copyright 2016 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Contains flags that we want to apply not only to Chromium APKs, but also to +# third-party apps that bundle the Cronet library. + +# WARNING: rules in this file are applied to entire third-party APKs, not just +# Chromium code. They MUST be scoped appropriately to avoid side effects on app +# code that we do not own. + +# Keep all CREATOR fields within Parcelable that are kept. +-keepclassmembers class !cr_allowunused,org.chromium.** implements android.os.Parcelable { + public static *** CREATOR; +} + +# Don't obfuscate Parcelables as they might be marshalled outside Chrome. +# If we annotated all Parcelables that get put into Bundles other than +# for saveInstanceState (e.g. PendingIntents), then we could actually keep the +# names of just those ones. For now, we'll just keep them all. +-keepnames,allowaccessmodification class !cr_allowunused,org.chromium.** implements android.os.Parcelable {} + +# Keep all enum values and valueOf methods. See +# http://proguard.sourceforge.net/index.html#manual/examples.html +# for the reason for this. Also, see http://crbug.com/248037. +-keepclassmembers enum !cr_allowunused,org.chromium.** { + public static **[] values(); +} + +# Required to remove fields until b/274802355 is resolved. +-assumevalues class !cr_allowunused,** { + final org.chromium.base.ThreadUtils$ThreadChecker * return _NONNULL_; +} +# -------- Config Path: build/android/chromium_annotations.flags -------- +# Copyright 2022 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Contains flags related to annotations in //build/android that can be safely +# shared with Cronet, and thus would be appropriate for third-party apps to +# include. + +# Keep all annotation related attributes that can affect runtime +-keepattributes RuntimeVisible*Annotations +-keepattributes AnnotationDefault + +# Keeps for class level annotations. +-keep,allowaccessmodification @org.chromium.build.annotations.UsedByReflection class ** {} + +# Keeps for method level annotations. +-keepclasseswithmembers,allowaccessmodification class ** { + @org.chromium.build.annotations.UsedByReflection ; +} +-keepclasseswithmembers,allowaccessmodification class ** { + @org.chromium.build.annotations.UsedByReflection ; +} + +# Never inline classes, methods, or fields with this annotation, but allow +# shrinking and obfuscation. +# Relevant to fields when they are needed to store strong references to objects +# that are held as weak references by native code. +-if @org.chromium.build.annotations.DoNotInline class * { + *** *(...); +} +-keep,allowobfuscation,allowaccessmodification class <1> { + *** <2>(...); +} +-keepclassmembers,allowobfuscation,allowaccessmodification class * { + @org.chromium.build.annotations.DoNotInline ; +} +-keepclassmembers,allowobfuscation,allowaccessmodification class * { + @org.chromium.build.annotations.DoNotInline ; +} + +-alwaysinline class * { + @org.chromium.build.annotations.AlwaysInline *; +} + +# Keep all logs (Log.VERBOSE = 2). R8 does not allow setting to 0. +-maximumremovedandroidloglevel 1 class ** { + @org.chromium.build.annotations.DoNotStripLogs ; +} +-maximumremovedandroidloglevel 1 @org.chromium.build.annotations.DoNotStripLogs class ** { + ; +} + +# Never merge classes horizontally or vertically with this annotation. +# Relevant to classes being used as a key in maps or sets. +-keep,allowaccessmodification,allowobfuscation,allowshrinking @org.chromium.build.annotations.DoNotClassMerge class * + +# Mark members annotated with IdentifierNameString as identifier name strings +-identifiernamestring class * { + @org.chromium.build.annotations.IdentifierNameString *; +} + +# Mark fields with this to help R8 figure out that they cannot be null. +# Use assumevalues in addition to assumenosideeffects block because Google3 proguard cannot parse +# assumenosideeffects blocks which overwrite return value. +-assumevalues class ** { + @org.chromium.build.annotations.AssumeNonNull *** *(...) return _NONNULL_; +} +-assumenosideeffects class ** { + @org.chromium.build.annotations.AssumeNonNull *** *(...); +} +-assumevalues class ** { + @org.chromium.build.annotations.AssumeNonNull *** * return _NONNULL_; +} +-assumenosideeffects class ** { + @org.chromium.build.annotations.AssumeNonNull *** *; +} +# -------- Config Path: components/cronet/android/cronet_impl_common_proguard.cfg -------- +# Proguard config for apps that depend on cronet_impl_common_java.jar. + +# Used through reflection by the API code to figure out the version of the impl +# code it's talking to. +-keep public class org.chromium.net.impl.ImplVersion { + public *; +} + +-dontwarn com.google.errorprone.annotations.DoNotMock +# -------- Config Path: components/cronet/android/cronet_impl_native_proguard.cfg -------- +# Proguard config for apps that depend on cronet_impl_native_java.jar. + +# This constructor is called using the reflection from Cronet API (cronet_api.jar). +-keep class org.chromium.net.impl.NativeCronetProvider { + public (android.content.Context); +} + +# While Chrome doesn't need to keep these with their version of R8, some cronet +# users may be on other optimizers which still require the annotation to be +# kept in order for the keep rules to work. +-keep @interface org.chromium.build.annotations.DoNotInline +-keep @interface org.chromium.build.annotations.UsedByReflection +-keep @interface org.chromium.build.annotations.IdentifierNameString +-keep @interface org.jni_zero.AccessedByNative +-keep @interface org.jni_zero.CalledByNative +-keep @interface org.jni_zero.CalledByNativeUnchecked + +# Suppress unnecessary warnings. +-dontnote org.chromium.net.ProxyChangeListener$ProxyReceiver +-dontnote org.chromium.net.AndroidKeyStore +# Needs 'void setTextAppearance(int)' (API level 23). +-dontwarn org.chromium.base.ApiCompatibilityUtils +# Needs 'boolean onSearchRequested(android.view.SearchEvent)' (API level 23). +-dontwarn org.chromium.base.WindowCallbackWrapper + +# Generated for chrome apk and not included into cronet. +-dontwarn org.chromium.base.multidex.ChromiumMultiDexInstaller +-dontwarn org.chromium.base.library_loader.LibraryLoader +-dontwarn org.chromium.base.SysUtils +-dontwarn org.chromium.build.NativeLibraries + +# Objects of this type are passed around by native code, but the class +# is never used directly by native code. Since the class is not loaded, it does +# not need to be preserved as an entry point. +-dontnote org.chromium.net.UrlRequest$ResponseHeadersMap +# https://android.googlesource.com/platform/sdk/+/marshmallow-mr1-release/files/proguard-android.txt#54 +-dontwarn android.support.** + +# This class should be explicitly kept to avoid failure if +# class/merging/horizontal proguard optimization is enabled. +-keep class org.chromium.base.CollectionUtil + +# Skip protobuf runtime check for isOnAndroidDevice(). +# A nice-to-have optimization shamelessly stolen from //third_party/protobuf/java/lite/proguard.pgcfg. +-assumevalues class com.google.protobuf.Android { + static boolean ASSUME_ANDROID return true; +} + +# See crbug.com/1440987. We must keep every native that we are manually +# registering. If Cronet bumps its min-sdk past 21, we may be able to move to +# automatic JNI registration. +-keepclasseswithmembers,includedescriptorclasses,allowaccessmodification class org.chromium.**,J.N { + native ; +} + +# Protobuf builder uses reflection so make sure ProGuard leaves it alone. See +# https://crbug.com/1395764. +# Note that we can't simply use the rule from +# //third_party/protobuf/java/lite/proguard.pgcfg, because some users who +# consume our ProGuard rules do not want all their protos to be kept. Instead, +# use a more specific rule that covers Chromium protos only. +-keepclassmembers class org.chromium.** extends com.google.protobuf.GeneratedMessageLite { + ; +} +# -------- Config Path: components/cronet/android/cronet_shared_proguard.cfg -------- +# Proguard config for apps that depend on cronet_shared_java.jar (which should +# be all apps that depend on any part of Cronet) + +# Part of the Android System SDK, so ProGuard won't be able to resolve it if +# running against the standard SDK. +-dontwarn android.util.StatsEvent +-dontwarn android.util.StatsEvent$* +# There is also an undefined reference to android.util.StatsLog.write(), which +# R8 appears to be fine with but other processors (e.g. internal Google +# ProGuard) may not be. See b/315269496. +-dontwarn android.util.StatsLog +# -------- Config Path: third_party/androidx/androidx_annotations.flags -------- +# Copyright 2023 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +-keep @androidx.annotation.Keep class * +-keepclasseswithmembers,allowaccessmodification class * { + @androidx.annotation.Keep ; +} +-keepclasseswithmembers,allowaccessmodification class * { + @androidx.annotation.Keep ; +} +# -------- Config Path: third_party/jni_zero/proguard.flags -------- +# Copyright 2023 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Keeps for method level annotations. +-keepclasseswithmembers,allowaccessmodification class ** { + @org.jni_zero.AccessedByNative ; +} +-keepclasseswithmembers,includedescriptorclasses,allowaccessmodification class ** { + @org.jni_zero.CalledByNative ; +} +-keepclasseswithmembers,includedescriptorclasses,allowaccessmodification class ** { + @org.jni_zero.CalledByNativeUnchecked ; +} + +# Allow unused native methods to be removed, but prevent renaming on those that +# are kept. +# TODO(crbug.com/315973491): Restrict the broad scope of this rule. +-keepclasseswithmembernames,includedescriptorclasses,allowaccessmodification class ** { + native ; +} diff --git a/app/cronetlib/cronet_api.jar b/app/cronetlib/cronet_api.jar new file mode 100755 index 0000000000000000000000000000000000000000..8f94ada454bc5eb35f8095d776890a9204451d6f GIT binary patch literal 176441 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
  • PGqq22q1i1 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 literal 0 HcmV?d00001 diff --git a/app/cronetlib/cronet_impl_native_java.jar b/app/cronetlib/cronet_impl_native_java.jar new file mode 100755 index 0000000000000000000000000000000000000000..933ae53dbf03e6700ba75d1d3271eb8e0fe9a3bd GIT binary patch literal 1419896 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.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 literal 0 HcmV?d00001 diff --git a/app/cronetlib/cronet_shared_java.jar b/app/cronetlib/cronet_shared_java.jar new file mode 100755 index 0000000000000000000000000000000000000000..dd60f7daf49a4a40e6329ac39f0c6868cb7c1cf7 GIT binary patch literal 78508 zcmeEv30zypwfD?rkq{RHx`2&sY_PG70dII;Vp|Ad%YeX=u#KHWkwLbVfaD-y$H_~Y zv{{<&dzz$enzT*2xNTy`gfz|4q)oc-ulwG!?dyA8US8UK|1)#1bR{l}Fz)-l z-|urG&fI%v&g|!$IdkSrPe)#U1(W~k8h0IB_SX+{#^~3Km+|4_Yx++N$A_XLLu+D@ zWTk=6Z!;Y8x|fB(c&NOBpgvN46T<{G9N~bcvz@{yo(89|zOY&&NKr>Hb&k13+|#0R7OXON&U(B12IdzPb8EDyf|{HN|6@#B%Ii>b*d682GkRT&K!3 zimMImQo=qhCJ7uj0|I_#**Q|?vpeC6BJ>Y z%9a!6#Y4VG@^pOon*Af;!D#Y~CxLMtM!`*aHK=SQS7EA`fBP|Ei%3%Ia`DYI7#ned?YivjI+7@V%A#p)B0#)1``Cc^e_s4 zJd!j`jf#eq=NFyx{M8u^39&=d+fHJ1mR6B0KRTmWexASE|m696%UhjUUY#Bo{0a=P{p% z)y;YY+|L4l4j{3!{Vd3eGR3{T3$J>%6w`oyengy_PFah4I_r-NkmOhI8c9Nw$`Sy# ziG{$7nX*+5MW~h`Pv*ILzzTe`iXT!P+Kngy1lD|SS5GIsC{{rnO0eFjKj7-@^#nNU zJ~F%6I*6XzScwX8-3e0MKL~LfB3%Y=m20Ez4wB(*tV(5b**x&k^b*7q8yP|=#fqaF z(%8lZ6K57|X1nQyF2|a=Hz5%C=c{Z1TWAu{tIVkMl-`vG?5u!B+I&dv1nX3`ge|3p z5@i5S+uyKqmYWKNGLTOwB_6dX?aJBhv57>{Sa?W?sp($PTMOe!im63`=9c(&XILpQ3M|&~ zSMu~-h(DLD(Bp3azo!%qCvO~M+B1wx#$t}&fbUWUa9}Cbh$n^S68=(->mZFwpr3O4yM3NuNUDuOy}m$? z$L($RdfKq0oj%Qz=!90ILS>c2g$ffwy*Jr9lGgi$VZr=oofU(^_EV!ljS(2>?~f!B z$3_OB!!l9G)_8m{5+*g9BYZM?ATpdlWN3e;Uu``NFcg!XD}^F&BnpktgbZdfHN>9j z=bp9H%b;pGQtFj6q14kPud1ZMa){X2YR+b-in`dCqTe>QjilcbhAKqWn39POak1~VuJ?XJgDo?rtCSj-AMDXc-Th$Tp2N4N>a zoCDYl#

    ;6uz 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/羊皮纸4.jpg b/app/src/main/assets/bg/羊皮纸4.jpg new file mode 100755 index 0000000000000000000000000000000000000000..9ca2d62eba8d7adeebc728fdc2af30a06be0d5e1 GIT binary patch literal 110907 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/边彩画布.jpg b/app/src/main/assets/bg/边彩画布.jpg new file mode 100755 index 0000000000000000000000000000000000000000..492977de533101786b193b541048fb23de98df78 GIT binary patch literal 76772 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/cronet.json b/app/src/main/assets/cronet.json new file mode 100755 index 000000000..fa4e3399d --- /dev/null +++ b/app/src/main/assets/cronet.json @@ -0,0 +1 @@ +{"x86":"2288814a4d9d4ee24154f52abf227fa0","armeabi-v7a":"ac8190c5e795d1a754b7bf2d477119c5","x86_64":"116f2d0f8e6363caacbd5c38bfc45b9f","arm64-v8a":"61a96a20241e56ac7ab107cc3e4c3fda","version":"128.0.6613.40"} \ No newline at end of file diff --git a/app/src/main/assets/defaultData/bookSources.json b/app/src/main/assets/defaultData/bookSources.json new file mode 100755 index 000000000..386a1e8be --- /dev/null +++ b/app/src/main/assets/defaultData/bookSources.json @@ -0,0 +1,48 @@ +[ + { + "bookSourceComment": "", + "bookSourceGroup": "听书", + "bookSourceName": "消消乐听书", + "bookSourceType": 1, + "bookSourceUrl": "https://www.kaixin7days.com", + "customOrder": 0, + "enabled": true, + "enabledExplore": true, + "exploreUrl": "@js:var header = JSON.parsesource.getLoginHeader()\nvar json = ''\nvar j = null\nif (header != null) {\n json = java.connect('https://www.kaixin7days.com/book-service/bookMgt/getBookCategroy,{\"method\":\"POST\",\"body\":{}}', header).body()\n j = JSON.parse(json)\n}\nif (j == null || j.statusCode != 200) {\n json = java.connect('https://www.kaixin7days.com/visitorLogin,{\"method\":\"POST\", \"body\":{} }').body()\n j = JSON.parse(json)\n var accessToken = {\n Authorization: 'Bearer ' + j.content.accessToken\n }\n header = JSON.stringify(accessToken)\n source.putLoginHeader(header)\n json = java.connect('https://www.kaixin7days.com/book-service/bookMgt/getBookCategroy,{\"method\":\"POST\",\"body\":{} }', header).body()\n j = JSON.parse(json)\n}\nvar fls = j.content\nvar fx = []\nfor (var i = 0; i < fls.length; i++) {\n fx.push({\n title: fls[i].categoryName,\n url: '/book-service/bookMgt/getAllBookByCategroyId,{\"method\":\"POST\",\"body\":{\"categoryIds\": \"' + fls[i].associationCategoryIDs + '\",\"pageNum\": {{page}},\"pageSize\": 100}}'\n })\n}\nJSON.stringify(fx)", + "searchUrl": "https://www.kaixin7days.com/book-service/bookMgt/findBookName,{\"method\":\"POST\",\"body\":{\"title\": \"searchKey\",\"pageNum\": {{searchPage}},\"pageSize\": 100}}", + "lastUpdateTime": 1630656684531, + "loginCheckJs": "var strRes = result\nvar c = JSON.parse(result.body())\nif (c.statusCode == 301) {\n var loginInfo = source.getLoginInfo()\n var dl = null\n if (loginInfo) {\n dl = java.connect('https://www.kaixin7days.com/login,{\"method\":\"POST\",\"body\":' + loginInfo + '}').body()\n } else {\n dl = java.connect('https://www.kaixin7days.com/visitorLogin,{\"method\":\"POST\",\"body\":{}}').body()\n }\n c = JSON.parse(dl)\n var accessToken = {\n Authorization: \"Bearer \" + c.content.accessToken\n }\n var header = JSON.stringify(accessToken)\n source.putLoginHeader(header)\n strRes = java.connect(url, header)\n}\nstrRes", + "loginUi": "[{\"name\": \"telephone\",\"type\": \"text\"},{\"name\": \"password\",\"type\": \"password\"},{\"name\": \"注册\",\"type\": \"button\",\"action\": \"http://www.yooike.com/xiaoshuo/#/register?title=%E6%B3%A8%E5%86%8C\"}]", + "loginUrl": "var loginInfo = source.getLoginInfo()\nvar json = java.connect('https://www.kaixin7days.com/login,{\"method\":\"POST\",\"body\":' + loginInfo + '}').body()\nvar loginRes = JSON.parse(json)\nvar header = null\nif (loginRes.statusCode == 200) {\n var accessToken = {\n Authorization: \"Bearer \" + loginRes.content.accessToken\n }\n header = JSON.stringify(accessToken)\n source.putLoginHeader(header)\n}\nheader", + "respondTime": 180000, + "ruleBookInfo": {}, + "ruleContent": { + "content": "", + "payAction": "var header = JSON.parse(source.getLoginHeader()); var bookId = book.getVariableMap().get('bookId');var chapterId = java.get('chapterId');\n'http://www.shuidi.online/?name=' + book.getName() + '&type=2&cover=' + book.getCoverUrl() + '&chapterId=' + chapterId + '&chapter=203&allNumber=' + book.getTotalChapterNum() + '&bookId=' + bookId + '&chapterIds=' + chapterId + '&number=' + chapter.getIndex() + '&accessToken=' + header.Authorization.substring(7) + '#/pay'" + }, + "ruleExplore": { + "author": "$.author", + "bookList": "$.content.content", + "bookUrl": "$.id@js:java.put('bookId', result);'https://www.kaixin7days.com/book-service/bookMgt/getAllChapterByBookId,{ \"method\": \"POST\",\"body\": {\"bookId\": \"'+result+'\",\"pageNum\": \"1\",\"pageSize\": \"10000\"} }'", + "coverUrl": "$.cover@js:var cover = JSON.parse(result);'https://www.shuidi.online/fileMgt/getPicture?filePath='+cover.storeFilePath", + "intro": "$.desc", + "lastChapter": "$.newestChapter", + "name": "$.title" + }, + "ruleSearch": { + "author": "$.author", + "bookList": "$.content.content", + "bookUrl": "$.id@js:java.put('bookId', result);'https://www.kaixin7days.com/book-service/bookMgt/getAllChapterByBookId,{ \"method\": \"POST\",\"body\": {\"bookId\": \"'+result+'\",\"pageNum\": \"1\",\"pageSize\": \"10000\"} }'", + "coverUrl": "$.cover@js:var cover = JSON.parse(result);'https://www.shuidi.online/fileMgt/getPicture?filePath='+cover.storeFilePath", + "intro": "$.desc", + "lastChapter": "$.newestChapter", + "name": "$.title" + }, + "ruleToc": { + "chapterList": "$.content.content", + "chapterName": "$.chapterTitle", + "chapterUrl": "$.id@js:java.put('chapterId', result);'https://www.shuidi.online/fileMgt/getAudioByChapterId?bookId=' + java.get('bookId') + '&chapterId=' + result + \"&pageNum=1&pageSize=50&keyId={{var header = JSON.parse(source.getLoginHeader());var keyId = '1632746188011002';var ks = java.md5Encode(keyId + java.get('chapterId') + header.Authorization);keyId + '&keySecret=' + ks}\" + '}'" + }, + "weight": 0 + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/coverRule.json b/app/src/main/assets/defaultData/coverRule.json new file mode 100755 index 000000000..5efef1ee6 --- /dev/null +++ b/app/src/main/assets/defaultData/coverRule.json @@ -0,0 +1,5 @@ +{ + "enable": false, + "searchUrl": "", + "coverRule": "" +} \ No newline at end of file diff --git a/app/src/main/assets/defaultData/dictRules.json b/app/src/main/assets/defaultData/dictRules.json new file mode 100755 index 000000000..4ec6300ab --- /dev/null +++ b/app/src/main/assets/defaultData/dictRules.json @@ -0,0 +1,23 @@ +[ + { + "name": "百度汉语", + "urlRule": "https://dict.baidu.com/s?wd={{key}}", + "showRule": "@js:var jsoup = org.jsoup.Jsoup.parse(result)\njsoup.select(\"script,#search-bar,#right-panel,#copyright\").remove()\njsoup.select(\"#poem-list-items\").html()", + "enabled": true, + "sortNumber": 2 + }, + { + "name": "海词英文", + "urlRule": "https://apii.dict.cn/mini.php?q={{key}}", + "showRule": "tag.body@all", + "enabled": true, + "sortNumber": 1 + }, + { + "name": "海词中文", + "urlRule": "https://hanyu.dict.cn/{{key}}", + "showRule": "@js:var jsoup = org.jsoup.Jsoup.parse(result)\njsoup.select(\"script,#header,#footer,#page-share,.mslide,.title,#dictHcBtn,#dictHcBtnTop,#dictHc,#dictHc,#dictHcSettingArea,#dictHcClosetip\").remove()\njsoup.select(\"#cy\").html()", + "enabled": true, + "sortNumber": 0 + } +] diff --git a/app/src/main/assets/defaultData/directLinkUpload.json b/app/src/main/assets/defaultData/directLinkUpload.json new file mode 100755 index 000000000..57c5551e7 --- /dev/null +++ b/app/src/main/assets/defaultData/directLinkUpload.json @@ -0,0 +1,20 @@ +[ + { + "uploadUrl": "https://sy.nyasama.net/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}", + "downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.nyasama.net/shuyuan/' + result", + "summary": "喵公子网盘①(有效期30天·香港G口)", + "compress": false + }, + { + "uploadUrl": "https://sy.mgz6.com/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}", + "downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.com/shuyuan/' + result", + "summary": "喵公子网盘②(有效期7天)", + "compress": false + }, + { + "uploadUrl": "http://v2.jt12.eu/up-v2.php,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}", + "downloadUrlRule": "$.msg", + "summary": "橘涂书源网盘2.0 Beta(永久有效)", + "compress": false + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/httpTTS.json b/app/src/main/assets/defaultData/httpTTS.json new file mode 100755 index 000000000..065b5cb9f --- /dev/null +++ b/app/src/main/assets/defaultData/httpTTS.json @@ -0,0 +1,30 @@ +[ + { + "id": -100, + "name": "1.百度", + "url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=160&vol=5&aue=6&pit=5&_res_tag_=audio\"\n}", + "contentType": "audio/wav" + }, + { + "id": -29, + "name": "2.阿里云语音", + "url": "https://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/tts,{\"method\": \"POST\",\"body\": {\"appkey\":\"{{source.getLoginInfoMap().get('AppKey')}}\",\"text\":\"{{speakText}}\",\"format\":\"mp3\",\"volume\":100,\"speech_rate\":{{String((speakSpeed) * 20 - 400)}} }}", + "contentType": "audio/mpeg", + "loginUrl": "function login(){var loginInfo = source.getLoginInfoMap();\nvar accessKeyId = loginInfo.get('AccessKeyId');\nvar accessKeySecret = loginInfo.get('AccessKeySecret');\nvar timestamp = java.timeFormatUTC(new Date().getTime(), \"yyyy-MM-dd'T'HH:mm:ss'Z'\", 0);\nvar aly = new JavaImporter(Packages.javax.crypto.Mac, Packages.javax.crypto.spec.SecretKeySpec, Packages.javax.xml.bind.DatatypeConverter, Packages.java.net.URLEncoder, Packages.java.lang.String, Packages.android.util.Base64);\nwith (aly) {\n function percentEncode(value) {\n return URLEncoder.encode(value, \"UTF-8\").replace(\"+\", \"%20\")\n .replace(\"*\", \"%2A\").replace(\"%7E\", \"~\")\n }\n\n function sign(stringToSign, accessKeySecret) {\n var mac = Mac.getInstance('HmacSHA1');\n mac.init(new SecretKeySpec(String(accessKeySecret + '&').getBytes(\"UTF-8\"), \"HmacSHA1\"));\n var signData = mac.doFinal(String(stringToSign).getBytes(\"UTF-8\"));\n var signBase64 = Base64.encodeToString(signData, Base64.NO_WRAP);\n var signUrlEncode = percentEncode(signBase64);\n return signUrlEncode;\n }\n}\nvar query = 'AccessKeyId=' + accessKeyId + '&Action=CreateToken&Format=JSON&RegionId=cn-shanghai&SignatureMethod=HMAC-SHA1&SignatureNonce=' + java.randomUUID() + '&SignatureVersion=1.0&Timestamp=' + percentEncode(timestamp) + '&Version=2019-02-28';\nvar signStr = sign('GET&' + percentEncode('/') + '&' + percentEncode(query), accessKeySecret);\nvar queryStringWithSign = \"Signature=\" + signStr + \"&\" + query;\nvar body = java.ajax('http://nls-meta.cn-shanghai.aliyuncs.com/?' + queryStringWithSign)\nvar res = JSON.parse(body)\nif (res.Message) {\n throw new Error(res.Message)\n}\nvar header = { \"X-NLS-Token\": res.Token.Id };\nsource.putLoginHeader(JSON.stringify(header))}", + "loginUi": [ + { + "name": "AppKey", + "type": "text" + }, + { + "name": "AccessKeyId", + "type": "text" + }, + { + "name": "AccessKeySecret", + "type": "text" + } + ], + "loginCheckJs": "var response = result;\nif (response.headers().get(\"Content-Type\") != \"audio/mpeg\") {\n var body = JSON.parse(response.body().string())\n if (body.status == 40000001) {\n source.login()\n java.getHeaderMap().putAll(source.getHeaderMap(true))\n response = java.getResponse()\n } else {\n throw body.message\n }\n}\nresponse" + } +] diff --git a/app/src/main/assets/defaultData/keyboardAssists.json b/app/src/main/assets/defaultData/keyboardAssists.json new file mode 100755 index 000000000..5b593c34b --- /dev/null +++ b/app/src/main/assets/defaultData/keyboardAssists.json @@ -0,0 +1,162 @@ +[ + { + "key": "@css:", + "value": "@css:", + "serialNo": 0 + }, + { + "key": "", + "value": "", + "serialNo": 1 + }, + { + "key": "{{}}", + "value": "{{}}", + "serialNo": 2 + }, + { + "key": "##", + "value": "##", + "serialNo": 3 + }, + { + "key": "&&", + "value": "&&", + "serialNo": 4 + }, + { + "key": "%%", + "value": "%%", + "serialNo": 5 + }, + { + "key": "||", + "value": "||", + "serialNo": 6 + }, + { + "key": "//", + "value": "//", + "serialNo": 7 + }, + { + "key": "\\", + "value": "\\", + "serialNo": 8 + }, + { + "key": "$.", + "value": "$.", + "serialNo": 9 + }, + { + "key": "@", + "value": "@", + "serialNo": 10 + }, + { + "key": ":", + "value": ":", + "serialNo": 11 + }, + { + "key": "class", + "value": "class", + "serialNo": 12 + }, + { + "key": "text", + "value": "text", + "serialNo": 13 + }, + { + "key": "href", + "value": "href", + "serialNo": 14 + }, + { + "key": "textNodes", + "value": "textNodes", + "serialNo": 15 + }, + { + "key": "ownText", + "value": "ownText", + "serialNo": 16 + }, + { + "key": "all", + "value": "all", + "serialNo": 17 + }, + { + "key": "html", + "value": "html", + "serialNo": 18 + }, + { + "key": "[", + "value": "[", + "serialNo": 19 + }, + { + "key": "]", + "value": "]", + "serialNo": 20 + }, + { + "key": "<", + "value": "<", + "serialNo": 21 + }, + { + "key": ">", + "value": ">", + "serialNo": 22 + }, + { + "key": "#", + "value": "#", + "serialNo": 23 + }, + { + "key": "!", + "value": "!", + "serialNo": 24 + }, + { + "key": ".", + "value": ".", + "serialNo": 25 + }, + { + "key": "+", + "value": "+", + "serialNo": 26 + }, + { + "key": "-", + "value": "-", + "serialNo": 27 + }, + { + "key": "*", + "value": "*", + "serialNo": 28 + }, + { + "key": "/", + "value": "/", + "serialNo": 29 + }, + { + "key": "=", + "value": "=", + "serialNo": 30 + }, + { + "key": "useWebView", + "value": ",{\"webView\": true}", + "serialNo": 31 + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/readConfig.json b/app/src/main/assets/defaultData/readConfig.json new file mode 100755 index 000000000..110f31fb0 --- /dev/null +++ b/app/src/main/assets/defaultData/readConfig.json @@ -0,0 +1,105 @@ +[ + { + "bgStr": "#ffc0edc6", + "bgStrEInk": "#FFFFFF", + "bgStrNight": "#000000", + "bgType": 0, + "bgTypeEInk": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconEInk": true, + "darkStatusIconNight": false, + "footerMode": 0, + "footerPaddingBottom": 10, + "footerPaddingLeft": 13, + "footerPaddingRight": 17, + "footerPaddingTop": 0, + "headerMode": 0, + "headerPaddingBottom": 0, + "headerPaddingLeft": 19, + "headerPaddingRight": 16, + "headerPaddingTop": 10, + "letterSpacing": 0, + "lineSpacingExtra": 10, + "name": "微信读书", + "paddingBottom": 4, + "paddingLeft": 22, + "paddingRight": 22, + "paddingTop": 5, + "paragraphIndent": "  ", + "paragraphSpacing": 6, + "showFooterLine": true, + "showHeaderLine": true, + "textBold": 0, + "textColor": "#ff0b0b0b", + "textColorEInk": "#000000", + "textColorNight": "#ADADAD", + "textSize": 24, + "tipColor": -10461088, + "tipFooterLeft": 7, + "tipFooterMiddle": 0, + "tipFooterRight": 6, + "tipHeaderLeft": 1, + "tipHeaderMiddle": 0, + "tipHeaderRight": 2, + "titleBottomSpacing": 0, + "titleMode": 0, + "titleSize": 4, + "titleTopSpacing": 0 + }, + { + "name": "预设1", + "bgStr": "#FFFFFF", + "bgStrNight": "#000000", + "textColor": "#000000", + "textColorNight": "#FFFFFF", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconNight": false + }, + { + "name": "预设2", + "bgStr": "#DDC090", + "bgStrNight": "#3C3F43", + "textColor": "#3E3422", + "textColorNight": "#DCDFE1", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": true, + "darkStatusIconNight": false + }, + { + "name": "预设3", + "bgStr": "#C2D8AA", + "bgStrNight": "#3C3F43", + "textColor": "#596C44", + "textColorNight": "#88C16F", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + }, + { + "name": "预设4", + "bgStr": "#DBB8E2", + "bgStrNight": "#3C3F43", + "textColor": "#68516C", + "textColorNight": "#F6AEAE", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + }, + { + "name": "预设5", + "bgStr": "#ABCEE0", + "bgStrNight": "#3C3F43", + "textColor": "#3D4C54", + "textColorNight": "#90BFF5", + "bgType": 0, + "bgTypeNight": 0, + "darkStatusIcon": false, + "darkStatusIconNight": false + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/rssSources.json b/app/src/main/assets/defaultData/rssSources.json new file mode 100755 index 000000000..220830854 --- /dev/null +++ b/app/src/main/assets/defaultData/rssSources.json @@ -0,0 +1,42 @@ +[ + { + "customOrder": 2, + "enableJs": true, + "enabled": true, + "singleUrl": true, + "sourceGroup": "legado", + "sourceIcon": "https://cdn.jsdelivr.net/gh/gedoor/legado@master/app/src/main/res/mipmap-hdpi/ic_launcher.png", + "sourceName": "使用说明", + "sourceUrl": "https://www.yuque.com/legado" + }, + { + "customOrder": 3, + "enableJs": true, + "enabled": true, + "singleUrl": true, + "sourceGroup": "legado", + "sourceIcon": "http://mmbiz.qpic.cn/mmbiz_png/hpfMV8hEuL2eS6vnCxvTzoOiaCAibV6exBzJWq9xMic9xDg3YXAick87tsfafic0icRwkQ5ibV0bJ84JtSuxhPuEDVquA/0?wx_fmt=png", + "sourceName": "小说拾遗", + "sourceUrl": "snssdk1128://user/profile/562564899806367" + }, + { + "customOrder": 4, + "enableJs": true, + "enabled": true, + "singleUrl": true, + "sourceGroup": "legado", + "sourceIcon": "https://cdn.jsdelivr.net/gh/mgz0227/meowcloud/icon.png", + "sourceName": "Meow云", + "sourceUrl": "https://pan.miaogongzi.net" + }, + { + "customOrder": 5, + "enableJs": true, + "enabled": true, + "singleUrl": true, + "sourceGroup": "legado", + "sourceIcon": "https://cdn.jsdelivr.net/gh/gedoor/legado@master/app/src/main/res/mipmap-hdpi/ic_launcher.png", + "sourceName": "烏雲净化", + "sourceUrl": "https://www.lanzoux.com/b0bw8jwoh" + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/themeConfig.json b/app/src/main/assets/defaultData/themeConfig.json new file mode 100755 index 000000000..3ff7f7cc1 --- /dev/null +++ b/app/src/main/assets/defaultData/themeConfig.json @@ -0,0 +1,34 @@ +[ + { + "themeName": "默认", + "isNightTheme": false, + "primaryColor": "#795548", + "accentColor": "#E53935", + "backgroundColor": "#F5F5F5", + "bottomBackground": "#EEEEEE" + }, + { + "themeName": "典雅蓝", + "isNightTheme": false, + "primaryColor": "#03A9F4", + "accentColor": "#AD1457", + "backgroundColor": "#F5F5F5", + "bottomBackground": "#EEEEEE" + }, + { + "themeName": "黑白", + "isNightTheme": true, + "primaryColor": "#303030", + "accentColor": "#E0E0E0", + "backgroundColor": "#424242", + "bottomBackground": "#424242" + }, + { + "themeName": "A屏黑", + "isNightTheme": true, + "primaryColor": "#000000", + "accentColor": "#FFFFFF", + "backgroundColor": "#000000", + "bottomBackground": "#000000" + } +] \ No newline at end of file diff --git a/app/src/main/assets/defaultData/txtTocRule.json b/app/src/main/assets/defaultData/txtTocRule.json new file mode 100755 index 000000000..15fd300a2 --- /dev/null +++ b/app/src/main/assets/defaultData/txtTocRule.json @@ -0,0 +1,210 @@ +[ + { + "id": -1, + "enable": true, + "name": "目录(去空白)", + "rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和]))).{0,30}$", + "example": "第一章 假装第一章前面有空白但我不要", + "serialNumber": 0 + }, + { + "id": -2, + "enable": true, + "name": "目录", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|篇(?!张))).{0,30}$", + "example": "第一章 标准的粤语就是这样", + "serialNumber": 1 + }, + { + "id": -3, + "enable": false, + "name": "目录(匹配简介)", + "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", + "example": "简介 老夫诸葛村夫", + "serialNumber": 2 + }, + { + "id": -4, + "enable": false, + "name": "目录(古典、轻小说备用)", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?![分赛游])|回(?![合来事去])|场(?![和合比电是])|话|篇(?!张))).{0,30}$", + "example": "第一章 比上面只多了回和话", + "serialNumber": 3 + }, + { + "id": -5, + "enable": false, + "name": "数字(纯数字标题)", + "rule": "(?<=[ \\s])\\d+\\.?[  \\t]{0,4}$", + "example": "12", + "serialNumber": 4 + }, + { + "id": -6, + "enable": false, + "name": "大写数字(纯数字标题)", + "rule": "(?<=[ \\s])[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,12}[  \\t]{0,4}$", + "example": "一百七十", + "serialNumber": 5 + }, + { + "id": -7, + "enable": false, + "name": "数字混合(纯数字标题)", + "rule": "(?<=[ \\s])[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟\\d]{1,12}[  \\t]{0,4}$", + "example": "12\n一百七十", + "serialNumber": 6 + }, + { + "id": -8, + "enable": true, + "name": "数字 分隔符 标题名称", + "rule": "^[  \\t]{0,4}\\d{1,5}[::,., 、_—\\-].{1,30}$", + "example": "1、这个就是标题", + "serialNumber": 7 + }, + { + "id": -9, + "enable": true, + "name": "大写数字 分隔符 标题名称", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章?)[ 、_—\\-].{1,30}$", + "example": "一、只有前面的数字有差别\n二十四章 我瞎编的标题", + "serialNumber": 8 + }, + { + "id": -10, + "enable": false, + "name": "数字混合 分隔符 标题名称", + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章?[ 、_—\\-]|\\d{1,5}章?[::,., 、_—\\-]).{0,30}$", + "example": "1、人参公鸡\n二百二十章 boy next door", + "serialNumber": 9 + }, + { + "id": -11, + "enable": true, + "name": "正文 标题/序号", + "rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$", + "example": "正文 我奶常山赵子龙", + "serialNumber": 10 + }, + { + "id": -12, + "enable": true, + "name": "Chapter/Section/Part/Episode 序号 标题", + "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][oO][.、]|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$", + "example": "Chapter 1 MyGrandmaIsNB", + "serialNumber": 11 + }, + { + "id": -13, + "enable": false, + "name": "Chapter(去简介)", + "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Nn][Oo]\\.|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$", + "example": "Chapter 1 MyGrandmaIsNB", + "serialNumber": 12 + }, + { + "id": -14, + "enable": true, + "name": "特殊符号 序号 标题", + "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", + "example": "【第一章 后面的符号可以没有", + "serialNumber": 13 + }, + { + "id": -15, + "enable": false, + "name": "特殊符号 标题(成对)", + "rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", + "example": "『加个直角引号更专业』\n(11)我奶常山赵子聋", + "serialNumber": 14 + }, + { + "id": -16, + "enable": true, + "name": "特殊符号 标题(单个)", + "rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", + "example": "☆、晋江作者最喜欢的格式", + "serialNumber": 15 + }, + { + "id": -17, + "enable": true, + "name": "章/卷 序号 标题", + "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", + "example": "卷五 开源盛世", + "serialNumber": 16 + }, + { + "id": -18, + "enable": false, + "name": "顶格标题", + "rule": "^\\S.{1,20}$", + "example": "20字以内顶格写的都是标题", + "serialNumber": 17 + }, + { + "id": -19, + "enable": false, + "name": "双标题(前向)", + "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", + "example": "第一章 真正的标题\n第一章 这个不要", + "serialNumber": 18 + }, + { + "id": -20, + "enable": false, + "name": "双标题(后向)", + "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", + "example": "第一章 这个标题不要\n第一章真正的标题", + "serialNumber": 19 + }, + { + "id": -21, + "enable": true, + "name": "书名 括号 序号", + "rule": "^[一-龥]{1,20}[  \\t]{0,4}[((][\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[))][  \\t]{0,4}$", + "example": "标题后面数字有括号(12)", + "serialNumber": 20 + }, + { + "id": -22, + "enable": true, + "name": "书名 序号", + "rule": "^[一-龥]{1,20}[  \\t]{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[  \\t]{0,4}$", + "example": "标题后面数字没有括号124", + "serialNumber": 21 + }, + { + "id": -23, + "enable": false, + "name": "特定字符 标题 特定符号", + "rule": "(?<=\\={3,6}).{1,40}?(?=\\=)", + "example": "===起这种标题干什么===", + "serialNumber": 22 + }, + { + "id": -24, + "enable": true, + "name": "字数分割 分节阅读", + "rule": "(?<=[  \\t]{0,4})(?:.{0,15}分[页节章段]阅读[-_ ]|第\\s{0,4}[\\d零一二两三四五六七八九十百千万]{1,6}\\s{0,4}[页节]).{0,30}$", + "example": "分节|分页|分段阅读\n第一页", + "serialNumber": 23 + }, + { + "id": -25, + "enable": false, + "name": "通用规则", + "rule": "(?im)^.{0,6}(?:[引楔]子|正文(?!完|结)|[引序前]言|[序终]章|扉页|[上中下][部篇卷]|卷首语|后记|尾声|番外|={2,4}|第\\s{0,4}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|页[、  ]|集(?![合和])|部(?![分是门落])|篇(?!张))).{0,40}$|^.{0,6}[\\d〇零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟a-z]{1,8}[、.  ].{0,20}$", + "example": "激进规则,适配更多非常用格式", + "serialNumber": 24 + }, + { + "id": -100, + "enable": false, + "name": "默认分章规则", + "rule": "", + "example": "兜底规则,请勿改动此内容", + "serialNumber": 99 + } +] diff --git a/app/src/main/assets/disclaimer.md b/app/src/main/assets/disclaimer.md new file mode 100755 index 000000000..5511655f5 --- /dev/null +++ b/app/src/main/assets/disclaimer.md @@ -0,0 +1,16 @@ +# 免责声明(Disclaimer) + +* 阅读是一款解析指定规则并获取内容的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 +* 当您搜索一本书的时,阅读会您所使用的规则将该书的书名以关键词的形式提交到各个第三方网络文学网站。 +各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。 +任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。 +第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。 +您应该对使用搜索引擎的结果自行承担风险。 +* 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。 +因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。 +阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 +* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。 +阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。 +阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。 +任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。 +阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。 \ No newline at end of file diff --git a/app/src/main/assets/epub/chapter.html b/app/src/main/assets/epub/chapter.html new file mode 100755 index 000000000..04024aaa7 --- /dev/null +++ b/app/src/main/assets/epub/chapter.html @@ -0,0 +1,13 @@ + + + + + Chapter + + + + +

    {title}

    +{content} + + diff --git a/app/src/main/assets/epub/cover.html b/app/src/main/assets/epub/cover.html new file mode 100755 index 000000000..cb6df6353 --- /dev/null +++ b/app/src/main/assets/epub/cover.html @@ -0,0 +1,21 @@ + + + + + Cover + + + +
    +

    {name}

    +
    {author} / 著
    + + \ No newline at end of file diff --git a/app/src/main/assets/epub/fonts.css b/app/src/main/assets/epub/fonts.css new file mode 100755 index 000000000..3457a6511 --- /dev/null +++ b/app/src/main/assets/epub/fonts.css @@ -0,0 +1,267 @@ +@charset "utf-8"; +/*---常用---*/ + +@font-face { + font-family: "zw"; + src: + local("宋体"),local("明体"),local("明朝"), + local("Songti"),local("Songti SC"),local("Songti TC"), /*iOS6+iBooks3*/ + local("Song S"),local("Song T"),local("STBShusong"),local("TBMincho"),local("HYMyeongJo"), /*Kindle Paperwihite*/ + local("DK-SONGTI"), + url(../Fonts/zw.ttf), + url(res:///opt/sony/ebook/FONT/zw.ttf), + url(res:///Data/FONT/zw.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/zw.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/zw.ttf), + url(res:///ebook/fonts/zw.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/zw.ttf), + url(res:///../../media/mmcblk0p1/fonts/zw.ttf), + url(file:///mnt/us/DK_System/system/fonts/zw.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/zw.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/zw.ttf), + url(res:///system/fonts/zw.ttf), + url(res:///system/media/sdcard/fonts/zw.ttf), + url(res:///media/fonts/zw.ttf), + url(res:///sdcard/fonts/zw.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/zw.ttf), + url(res:///media/flash/fonts/zw.ttf), + url(res:///media/sd/fonts/zw.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/zw.ttf), + url(res:///../fonts/zw.ttf), + url(../../../../../zw.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/zw.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/zw.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/zw.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} + +@font-face { + font-family: "fs"; + src: + local("amasis30"),local("仿宋"),local("仿宋_GB2312"), + local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), /*iOS6+iBooks3*/ + local("DK-FANGSONG"), + url(../Fonts/fs.ttf), + url(res:///opt/sony/ebook/FONT/fs.ttf), + url(res:///Data/FONT/fs.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/fs.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/fs.ttf), + url(res:///ebook/fonts/fs.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/fs.ttf), + url(res:///../../media/mmcblk0p1/fonts/fs.ttf), + url(file:///mnt/us/DK_System/system/fonts/fs.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/fs.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/fs.ttf), + url(res:///system/fonts/fs.ttf), + url(res:///system/media/sdcard/fonts/fs.ttf), + url(res:///media/fonts/fs.ttf), + url(res:///sdcard/fonts/fs.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/fs.ttf), + url(res:///media/flash/fonts/fs.ttf), + url(res:///media/sd/fonts/fs.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/fs.ttf), + url(res:///../fonts/fs.ttf), + url(../../../../../fs.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/fs.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/fs.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/fs.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} + +@font-face { + font-family: "kt"; + src: + local("Caecilia"),local("楷体"),local("楷体_GB2312"), + local("Kaiti"),local("Kaiti SC"),local("Kaiti TC"), /*iOS6+iBooks3*/ + local("MKai PRC"),local("MKaiGB18030C-Medium"),local("MKaiGB18030C-Bold"), /*Kindle Paperwihite*/ + local("DK-KAITI"), + url(../Fonts/kt.ttf), + url(res:///opt/sony/ebook/FONT/kt.ttf), + url(res:///Data/FONT/kt.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/kt.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/kt.ttf), + url(res:///ebook/fonts/kt.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/kt.ttf), + url(res:///../../media/mmcblk0p1/fonts/kt.ttf), + url(file:///mnt/us/DK_System/system/fonts/kt.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/kt.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/kt.ttf), + url(res:///system/fonts/kt.ttf), + url(res:///system/media/sdcard/fonts/kt.ttf), + url(res:///media/fonts/kt.ttf), + url(res:///sdcard/fonts/kt.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/kt.ttf), + url(res:///media/flash/fonts/kt.ttf), + url(res:///media/sd/fonts/kt.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/kt.ttf), + url(res:///../fonts/kt.ttf), + url(../../../../../kt.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/kt.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/kt.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/kt.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} + +@font-face { + font-family: "ht"; + src: + local("黑体"),local("微软雅黑"), + local("Heiti"),local("Heiti SC"),local("Heiti TC"), /*iOS6+iBooks3*/ + local("MYing Hei S"),local("MYing Hei T"),local("TBGothic"), /*Kindle Paperwihite*/ + local("DK-HEITI"), + url(../Fonts/ht.ttf), + url(res:///opt/sony/ebook/FONT/ht.ttf), + url(res:///Data/FONT/ht.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/ht.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/ht.ttf), + url(res:///ebook/fonts/ht.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/ht.ttf), + url(res:///../../media/mmcblk0p1/fonts/ht.ttf), + url(file:///mnt/us/DK_System/system/fonts/ht.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/ht.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/ht.ttf), + url(res:///system/fonts/ht.ttf), + url(res:///system/media/sdcard/fonts/ht.ttf), + url(res:///media/fonts/ht.ttf), + url(res:///sdcard/fonts/ht.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/ht.ttf), + url(res:///media/flash/fonts/ht.ttf), + url(res:///media/sd/fonts/ht.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/ht.ttf), + url(res:///../fonts/ht.ttf), + url(../../../../../ht.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/ht.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/ht.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/ht.ttf); + /*ADE1,8, 2.0 Windows Path*/; +} +@font-face { + font-family:"h1"; + src: + local("方正兰亭特黑长_GBK"),local("方正兰亭特黑长简体"),local("方正兰亭特黑长繁体"), + local("LantingTeheichang"), + local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), + local("DK-HEITI"), + url(../Fonts/h1.ttf), + url(res:///opt/sony/ebook/FONT/h1.ttf), + url(res:///Data/FONT/h1.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/h1.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/h1.ttf), + url(res:///ebook/fonts/h1.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/h1.ttf), + url(res:///../../media/mmcblk0p1/fonts/h1.ttf), + url(file:///mnt/us/DK_System/system/fonts/h1.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/h1.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/h1.ttf), + url(res:///system/fonts/h1.ttf), + url(res:///system/media/sdcard/fonts/h1.ttf), + url(res:///media/fonts/h1.ttf), + url(res:///sdcard/fonts/h1.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/h1.ttf), + url(res:///media/flash/fonts/h1.ttf), + url(res:///media/sd/fonts/h1.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/h1.ttf), + url(res:///../fonts/h1.ttf), + url(../../../../../h1.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/h1.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/h1.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/h1.ttf); /*ADE1,8, 2.0 Windows Path*/ +} +@font-face { + font-family:"h2"; + src: + local("方正大标宋_GBK"),local("方正大标宋简体"),local("方正大标宋繁体"), + local("Dabiaosong"), + local("Heiti"),local("Heiti SC"),local("Heiti TC"), + local("DK-XIAOBIAOSONG"), + url(../Fonts/h2.ttf), + url(res:///opt/sony/ebook/FONT/h2.ttf), + url(res:///Data/FONT/h2.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/h2.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/h2.ttf), + url(res:///ebook/fonts/h2.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/h2.ttf), + url(res:///../../media/mmcblk0p1/fonts/h2.ttf), + url(file:///mnt/us/DK_System/system/fonts/h2.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/h2.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/h2.ttf), + url(res:///system/fonts/h2.ttf), + url(res:///system/media/sdcard/fonts/h2.ttf), + url(res:///media/fonts/h2.ttf), + url(res:///sdcard/fonts/h2.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/h2.ttf), + url(res:///media/flash/fonts/h2.ttf), + url(res:///media/sd/fonts/h2.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/h2.ttf), + url(res:///../fonts/h2.ttf), + url(../../../../../h2.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/h2.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/h2.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/h2.ttf); /*ADE1,8, 2.0 Windows Path*/ +} + +@font-face { + font-family:"h3"; + src: + local("方正华隶_GBK"),local("方正行黑简体"),local("方正行黑繁体"), + local("Yuanti"),local("Yuanti SC"),local("Yuanti TC"), + local("DK-FANGSONG"), + url(../Fonts/h3.ttf), + url(res:///opt/sony/ebook/FONT/h3.ttf), + url(res:///Data/FONT/h3.ttf), + url(res:///opt/sony/ebook/FONT/tt0011m_.ttf), + url(res:///ebook/fonts/../../mnt/sdcard/fonts/h3.ttf), + url(res:///ebook/fonts/../../mnt/extsd/fonts/h3.ttf), + url(res:///ebook/fonts/h3.ttf), + url(res:///ebook/fonts/DroidSansFallback.ttf), + url(res:///fonts/ttf/h3.ttf), + url(res:///../../media/mmcblk0p1/fonts/h3.ttf), + url(file:///mnt/us/DK_System/system/fonts/h3.ttf), /*Duokan Old Path*/ + url(file:///mnt/us/DK_System/xKindle/res/userfonts/h3.ttf), /*Duokan 2012 Path*/ + url(res:///abook/fonts/h3.ttf), + url(res:///system/fonts/h3.ttf), + url(res:///system/media/sdcard/fonts/h3.ttf), + url(res:///media/fonts/h3.ttf), + url(res:///sdcard/fonts/h3.ttf), + url(res:///system/fonts/DroidSansFallback.ttf), + url(res:///mnt/MOVIFAT/font/h3.ttf), + url(res:///media/flash/fonts/h3.ttf), + url(res:///media/sd/fonts/h3.ttf), + url(res:///opt/onyx/arm/lib/fonts/AdobeHeitiStd-Regular.otf), + url(res:///../../fonts/h3.ttf), + url(res:///../fonts/h3.ttf), + url(../../../../../h3.ttf), /*EpubReaderI*/ + url(res:///mnt/sdcard/fonts/h3.ttf), /*Nook for Android: fonts in TF Card*/ + url(res:///fonts/h3.ttf), /*ADE1,8, 2.0 Program Path*/ + url(res:///../../../../Windows/fonts/h3.ttf); /*ADE1,8, 2.0 Windows Path*/ +} + +@font-face { + font-family:"luohua"; + src:local("汉仪落花体"), + url("../Fonts/hylh.ttf"); +} \ No newline at end of file diff --git a/app/src/main/assets/epub/intro.html b/app/src/main/assets/epub/intro.html new file mode 100755 index 000000000..5c64026f8 --- /dev/null +++ b/app/src/main/assets/epub/intro.html @@ -0,0 +1,11 @@ + + + + + Intro + + + + +

    内容简介

    {intro} + diff --git a/app/src/main/assets/epub/logo.png b/app/src/main/assets/epub/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..d0a3769ca601f01c833b9dbf86f542f75abee2b1 GIT binary patch literal 24175 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 KMNUMnLSTYVE;SVZ literal 0 HcmV?d00001 diff --git a/app/src/main/assets/epub/main.css b/app/src/main/assets/epub/main.css new file mode 100755 index 000000000..b74f6ac93 --- /dev/null +++ b/app/src/main/assets/epub/main.css @@ -0,0 +1,551 @@ +@charset "utf-8"; +@import url("../Styles/fonts.css"); +body { + padding: 0%; + margin-top: 0%; + margin-bottom: 0%; + margin-left: 0.5%; + margin-right: 0.5%; + line-height: 130%; + text-align: justify; + font-family: "DK-SONGTI","st","宋体","zw",sans-serif; +} + +p { + text-align: justify; + text-indent: 2em; + line-height: 130%; + margin-right: 0.5%; + margin-left: 0.5%; + font-family: "DK-SONGTI","st","宋体","zw",sans-serif; +} +p.kaiti { + font-family: "DK-KAITI","kt","楷体","zw",serif; +} + +p.fangsong { + font-family: "DK-FANGSONG","fs","仿宋","zw",serif; +} + +span.xinli { + font-family: "DK-KAITI","kt","楷体","zw",serif; + color: #4e753f; +} +/** 英文斜体字 **/ +span.english{ + font-style: italic; +} +div { + margin: 0px; + padding: 0px; + line-height: 120%; + text-align: justify; + font-family: "zw"; +} +div.foot { + text-indent: 2em; + margin: 30% 5% 0 5%; + padding: 8px 0; +} +p.foot { + font-family: "DK-KAITI","kt","楷体","zw",serif; +} + +/*扉页*/ +.booksubtitle { + padding: 10px 0 0px 0; + text-indent: 0em; + font-size: 75%; + font-family: "ht"; +} + +.booktitle { + padding: 9% 0 0 0; + font-size: 1.3em; + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + font-weight: normal; + text-indent: 0em; + color: #000; + text-align: center; + line-height: 1.6; +} + +.booktitle0 { + font-size: 1.2em; + font-family: "fs"; + text-indent: 0em; + text-align: center; + line-height: 1.8; +} + +.booktitle1 { + padding: 0 0 0 0; + font-size: 0.85em; + font-family: "fs"; + text-indent: 0em; + text-align: center; + line-height: 1.6; +} + +.bookauthor { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + padding: 5% 5px 0px 5px; + text-indent: 0em; + text-align: center; + color: #000; + font-size: 90%; + line-height: 1.3; +} + +.booktranslator { + padding: 1% 5px 0px 5px; + text-indent: 0em; + text-align: center; + font-size: 85%; + line-height: 1.3; +} + +.bookpub { + font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; + padding: 30% 5px 5px 5px; + text-indent: 0em; + color: #000; + text-align: center; + font-size: 80%; +} + +/*标题页*/ +body.head { + background-repeat:no-repeat no-repeat; + background-size:160px 229px; + background-position:bottom right; + background-attachment:fixed; +} + +body.xhead { + background-color: #FDF5E6; +} + +h1.head { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 1.2em; + font-weight: bold; + color: #311a02; + text-indent: 0em; + font-weight: normal; + duokan-text-indent: 0em; + padding: auto; + text-align: center; + margin-top: -8em; +} + +div.head { + border: solid 2px #ffffff; + padding: 2px; + margin: 2em auto 0.7em auto; + text-align: center; + width: 1em; +} + +h1.head b { + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + font-weight: bold; + font-size: 1.2em; + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + color: #311a02; + margin: 0.5em auto; + line-height: 140%; +} + +div.back { + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + margin: 4em auto; +} + +img.back { + width: 70%; +} +img.back2 { + width: 40%; + margin: 2em 0 0 0; +} +/*正文*/ +/**楷体引文**/ +.titou { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; +} +.yinwen { + font-family: "DK-KAITI","kt","楷体","zw",serif; + margin-left: 2em; + text-indent: 0em; +} +.nicename { + font-family: "DK-HEITI",黑体,sans-serif; + font-weight: bold; + font-size: 0.9em; +} +body.head3 { + background-color: #a7bdcc; + color: #354f66; +} + +body.head4 { + background-color: #bfd19b; + color: #4e753f; +} + +h2.head { + font-family: "小标宋"; + text-align: left; + font-weight: bold; + font-size: 1.1em; + margin: 1em 2em 2em 0; + color: #3f83e8; + line-height: 140%; +} + +h2.head span { + font-family: "仿宋"; + font-size: 0.7em; + background-color: #3f83e8; + border-radius: 9px; + padding: 4px; + color: #fff; +} + + +div.logo { + margin: -2em 0% 0 0; + text-align: right; +} + +img.logo { + width: 40%; +} +.imgl { + /*图片居右*/ + margin: -8.8em 1em 4em 0em; + width: 80%; + text-align: right; +} + +h1.head { + line-height:130%; + font-size:1.4em; + text-align: center; + color: #BA2213; + font-weight: bold; + margin-top: 2em; + margin-bottom: 1em; + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + +} +h3 { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 1.1em; + margin: 1em 0; + border-left: 1.2em solid #00a1e9; + line-height: 120%; + padding-left: 3px; + color: #00a1e9; +} +h4 { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 1.1em; + text-align: center; + margin: 1em 0; + line-height: 120%; + color: #000; +} +h1.post { + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + text-align: center; + font-size: 1.3em; + color: #026fca; + margin: 3em auto 2em auto; +} +.banquan { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + text-align: left; + color: #000; + font-size:1.1em; + margin-bottom:1em; + text-indent: 1em; + duokan-text-indent: 1em; +} +p.post { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; +} +p.zy { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + margin: 1em 0 0 1em; + padding: 5px 0px 5px 10px; + text-indent: 0em; + border-left: 5px solid #a9b5c1; +} +.sign { + font-family: "DK-KAITI","kt","楷体","zw",serif; + margin: 1em 2px 0 auto; + text-align: right; + font-size: 0.8em; + text-indent: 0em; + duokan-text-indent: 0em; +} + +.mark { + font-family: "DK-HEITI",黑体,sans-serif; + font-size: 0.9em; + color: #fff; + text-indent: 0em; + duokan-text-indent: 0em; + background-color: maroon; + text-align: center; + padding: 0px; + margin: 2em 30%; +} + +/*出版社*/ +.chubanshe img{ + width:106px; + height:28px; +} +.chubanshe { + margin-top:20px; +} +.cr { + font-size:0.9em; +} + +/*多看画廊*/ +div.duokan-image-single { + text-align: center; + margin: 0.5em auto; /*插图盒子上下外边距为0.5em,左右设置auto是为了水平居中这个盒子*/ +} +img.picture-80 { + margin: 0; /*清除img元素的外边距*/ + width: 80%; /*预览窗口的宽度*/ + box-shadow: 3px 3px 10px #bfbfbf; /*给图片添加阴影效果*/ +} +p.duokan-image-maintitle { + margin: 1em 0 0; /*图片说明的段间距*/ + font-family: "楷体"; /*图片说明使用的字体*/ + font-size: 0.9em; /*字体大小*/ + text-indent: 0; /*首行缩进为零,当你使用单标签p来指定首行缩进为2em时,记得在需要居中的文本中清除缩进,因为样式是叠加的*/ + text-align: center; /*图片说明水平居中*/ + color: #a52a2a; /*字体颜色*/ + line-height: 1.25em; /*行高,防止有很长的图片说明*/ +} + + +/*制作说明页*/ +body.description { + background-image: url(../Images/001.png); + background-position: bottom center; + background-repeat: no-repeat; + background-size: cover; + padding: 25% 10% 0; + font-size: 0.9em; +} + +div.description-body { + width: 55%; + padding: 2em 1.3em; + border-radius: 0.5em; + font-size: 0.9em; + border-style: solid; + border-color: #393939; + border-width: 0.3em; + border-radius: 5em; + background-color: #5a5a5a; + box-shadow: 2px 2px 3px #828281; +} + +h1.description-title { + text-align: center; + font-family: "黑体"; + font-size: 1.2em; + margin: 0 0 1em 0; + color: #FF9; + text-shadow: 1px 1px 0 black; +} + +p.description-text { + color: #f9ddd2; + font-family: "准圆"; + margin: 0; + text-align: justify; + text-indent: 0; + duokan-text-indent: 0; +} + +hr.description-hr { + margin: 0.5em -1em; + border-style: dotted; + border-color: #9C9; + border-width: 0.05em 0 0 0; +} + +p.tips { + text-align: justify; + text-indent: 0; + duokan-text-indent: 0; + font-family: "楷体"; + font-size: 0.7em; + color: #FFC; + margin: 0; +} + +/*版本说明页*/ +.ver { + font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif; + font-weight: bold; + font-size: 100%; + color: #000; + margin: 1em 0 1em 0; + text-align: center; +} + +.vertitle { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + font-size: 100%; + text-indent: 0em; + text-align: left; + duokan-text-indent: 0em; +} + +.vertxt { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + line-height: 100%; + font-size: 85%; + text-indent: 0em; + text-align: left; + duokan-text-indent: 0em; +} +.verchar { + font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; + text-align: left; + text-indent: 1em; + duokan-text-indent: 1em; + margin-bottom: 1em; + margin-top: 1em; +} +.vernote { + font-family: "DK-FANGSONG",仿宋,"fs","fangsong",sans-serif; + font-size: 75%; + color: #686d70; + text-indent: 0em; + text-align: left; + duokan-text-indent: 0em; + padding-bottom: 15px; +} + +.line { + border: dotted #A2906A; + border-width: 1px 0 0 0; +} + +.entry { + margin-left: 18px; + font-size: 83%; + color: #8fe0a3; + text-indent: 0em; + duokan-text-indent: 0em; +} +/*版权信息*/ +.vol { + text-indent: 0em; + text-align: center; + padding: 0.8em; + margin: 0 auto 3px auto; + color: #000; + font-family: "方正小标宋_GBK","DK-XIAOBIAOSONG"; + font-size: 130%; + text-shadow: none; +} + +.cp { + font-family: "DK-CODE","DK-XIHEITI",细黑体,"xihei",sans-serif; + color: #412938; + font-size: 70%; + text-align: left; + text-indent: 0em; + duokan-text-indent: 0em; +} + +.xchar { + font-family: "DK-KAITI","kt","楷体","楷体_gb2312"; + text-indent: 0em; + duokan-text-indent: 0em; +} +/*多看弹注*/ +sup img { + line-height: 100%; + width: auto; + height: 1.0em; + margin: 0em; + padding: 0em; + vertical-align: text-top; +} + +ol { + margin-bottom:0; + padding:0 auto; + list-style-type: decimal; +} +.hr { + width:50%; + margin:2em 0 0 0.5em; + padding:0; + height:2px; + background-color: #F3221D; +} + +.duokan-footnote-content{ + padding:0 auto; + text-align: left; +} + +.duokan-footnote-item { + font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif; + text-align: left; + font-size: 80%; + line-height: 100%; + clear: both; + color:#000; + list-style-type:decimal; +} + +li.duokan-footnote-item a { + font-family:"DK-HEITI"; + text-align: left; +} +a{ + text-decoration: none; + color: #222; +} + +a:hover {background: #81caf9} +a:active {background: yellow} +.duokan-image-maintitle { + font-family:"DK-HEITI",黑体,"hei",sans-serif; + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + font-size:90%; + color: #1F4150; + margin-top: 1em; +} + +.duokan-image-subtitle { + font-family:"DK-XIHEITI",细黑体,"xihei",sans-serif; + text-align: center; + text-indent: 0em; + duokan-text-indent: 0em; + font-size:70%; + color: #3A3348; + margin-top: 1em; +} \ No newline at end of file diff --git a/app/src/main/assets/font/number.ttf b/app/src/main/assets/font/number.ttf new file mode 100755 index 0000000000000000000000000000000000000000..f2804dbe9c8edbb1c875138151c477593b2080c1 GIT binary patch literal 6000 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.9.22 ksp->1.0.17 +* 界面绘制优化 +* Bug修复和其他优化 + +---- + +* [2023年日志](https://github.com/gedoor/legado/blob/record2023/app/src/main/assets/updateLog.md) +* [2022年日志](https://github.com/gedoor/legado/blob/record2022/app/src/main/assets/updateLog.md) +* [2021年日志](https://github.com/gedoor/legado/blob/record2021/app/src/main/assets/updateLog.md) diff --git a/app/src/main/assets/web/assets/css/main.css b/app/src/main/assets/web/assets/css/main.css new file mode 100755 index 000000000..0d9b00e17 --- /dev/null +++ b/app/src/main/assets/web/assets/css/main.css @@ -0,0 +1,3956 @@ + /* + Forty by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +*/ + +html, body, div, span, applet, object, +iframe, h1, h2, h3, h4, h5, h6, p, blockquote, +pre, a, abbr, acronym, address, big, cite, +code, del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, b, +u, i, center, dl, dt, dd, ol, ul, li, fieldset, +form, label, legend, table, caption, tbody, +tfoot, thead, tr, th, td, article, aside, +canvas, details, embed, figure, figcaption, +footer, header, hgroup, menu, nav, output, ruby, +section, summary, time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline;} + +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block;} + +body { + line-height: 1; + background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),url("../../images/bg.jpg") no-repeat center center fixed; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + + blockquote:before, blockquote:after, q:before, q:after { + content: ''; + content: none; + } + +table { + border-collapse: collapse; + border-spacing: 0; +} + +body { + -webkit-text-size-adjust: none; +} + +mark { + background-color: transparent; + color: inherit; +} + +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +input, select, textarea { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; +} + +/* Basic */ + + @-ms-viewport { + width: device-width; + } + + body { + -ms-overflow-style: scrollbar; + } + + @media screen and (max-width: 480px) { + + html, body { + min-width: 320px; + } + + } + + html { + box-sizing: border-box; + height: 100vh; + } + + *, *:before, *:after { + box-sizing: inherit; + } + + body { + /* background: #242943; */ + height: 100vh; + } + + body.is-preload *, body.is-preload *:before, body.is-preload *:after { + -moz-animation: none !important; + -webkit-animation: none !important; + -ms-animation: none !important; + animation: none !important; + -moz-transition: none !important; + -webkit-transition: none !important; + -ms-transition: none !important; + transition: none !important; + } + +/* Type */ + + body, input, select, textarea { + color: #ffffff; + font-family: "Source Sans Pro", Helvetica, sans-serif; + font-size: 17pt; + font-weight: 300; + letter-spacing: 0.025em; + line-height: 1.65; + } + + @media screen and (max-width: 1680px) { + + body, input, select, textarea { + font-size: 14pt; + } + + } + + @media screen and (max-width: 1280px) { + + body, input, select, textarea { + font-size: 12pt; + } + + } + + @media screen and (max-width: 360px) { + + body, input, select, textarea { + font-size: 11pt; + } + + } + + a { + -moz-transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; + -webkit-transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; + -ms-transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; + transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; + border-bottom: dotted 1px; + color: inherit; + text-decoration: none; + } + + a:hover { + border-bottom-color: transparent; + color: #9bf1ff !important; + } + + a:active { + color: #53e3fb !important; + } + + strong, b { + color: #ffffff; + font-weight: 600; + } + + em, i { + font-style: italic; + } + + p { + margin: 0 0 2em 0; + } + + h1, h2, h3, h4, h5, h6 { + color: #ffffff; + font-weight: 600; + line-height: 1.65; + margin: 0 0 1em 0; + } + + h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + color: inherit; + border-bottom: 0; + } + + h1 { + font-size: 2.5em; + } + + h2 { + font-size: 1.75em; + } + + h3 { + font-size: 1.35em; + } + + h4 { + font-size: 1.1em; + } + + h5 { + font-size: 0.9em; + } + + h6 { + font-size: 0.7em; + } + + @media screen and (max-width: 736px) { + + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.5em; + } + + h3 { + font-size: 1.25em; + } + + } + + sub { + font-size: 0.8em; + position: relative; + top: 0.5em; + } + + sup { + font-size: 0.8em; + position: relative; + top: -0.5em; + } + + blockquote { + border-left: solid 4px rgba(212, 212, 255, 0.1); + font-style: italic; + margin: 0 0 2em 0; + padding: 0.5em 0 0.5em 2em; + } + + code { + background: rgba(212, 212, 255, 0.035); + font-family: "Courier New", monospace; + font-size: 0.9em; + margin: 0 0.25em; + padding: 0.25em 0.65em; + } + + pre { + -webkit-overflow-scrolling: touch; + font-family: "Courier New", monospace; + font-size: 0.9em; + margin: 0 0 2em 0; + } + + pre code { + display: block; + line-height: 1.75; + padding: 1em 1.5em; + overflow-x: auto; + } + + hr { + border: 0; + border-bottom: solid 1px rgba(212, 212, 255, 0.1); + margin: 2em 0; + } + + hr.major { + margin: 3em 0; + } + + .align-left { + text-align: left; + } + + .align-center { + text-align: center; + } + + .align-right { + text-align: right; + } + +/* Row */ + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp { + order: -1; + } + + .row > .col-1 { + width: 8.33333%; + } + + .row > .off-1 { + margin-left: 8.33333%; + } + + .row > .col-2 { + width: 16.66667%; + } + + .row > .off-2 { + margin-left: 16.66667%; + } + + .row > .col-3 { + width: 25%; + } + + .row > .off-3 { + margin-left: 25%; + } + + .row > .col-4 { + width: 33.33333%; + } + + .row > .off-4 { + margin-left: 33.33333%; + } + + .row > .col-5 { + width: 41.66667%; + } + + .row > .off-5 { + margin-left: 41.66667%; + } + + .row > .col-6 { + width: 50%; + } + + .row > .off-6 { + margin-left: 50%; + } + + .row > .col-7 { + width: 58.33333%; + } + + .row > .off-7 { + margin-left: 58.33333%; + } + + .row > .col-8 { + width: 66.66667%; + } + + .row > .off-8 { + margin-left: 66.66667%; + } + + .row > .col-9 { + width: 75%; + } + + .row > .off-9 { + margin-left: 75%; + } + + .row > .col-10 { + width: 83.33333%; + } + + .row > .off-10 { + margin-left: 83.33333%; + } + + .row > .col-11 { + width: 91.66667%; + } + + .row > .off-11 { + margin-left: 91.66667%; + } + + .row > .col-12 { + width: 100%; + } + + .row > .off-12 { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.5em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.5em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.5em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.5em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -1em; + } + + .row.gtr-50 > * { + padding: 0 0 0 1em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -1em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 1em; + } + + .row { + margin-top: 0; + margin-left: -2em; + } + + .row > * { + padding: 0 0 0 2em; + } + + .row.gtr-uniform { + margin-top: -2em; + } + + .row.gtr-uniform > * { + padding-top: 2em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-150 > * { + padding: 0 0 0 3em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 3em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -4em; + } + + .row.gtr-200 > * { + padding: 0 0 0 4em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -4em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 4em; + } + + @media screen and (max-width: 1680px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-xlarge { + order: -1; + } + + .row > .col-1-xlarge { + width: 8.33333%; + } + + .row > .off-1-xlarge { + margin-left: 8.33333%; + } + + .row > .col-2-xlarge { + width: 16.66667%; + } + + .row > .off-2-xlarge { + margin-left: 16.66667%; + } + + .row > .col-3-xlarge { + width: 25%; + } + + .row > .off-3-xlarge { + margin-left: 25%; + } + + .row > .col-4-xlarge { + width: 33.33333%; + } + + .row > .off-4-xlarge { + margin-left: 33.33333%; + } + + .row > .col-5-xlarge { + width: 41.66667%; + } + + .row > .off-5-xlarge { + margin-left: 41.66667%; + } + + .row > .col-6-xlarge { + width: 50%; + } + + .row > .off-6-xlarge { + margin-left: 50%; + } + + .row > .col-7-xlarge { + width: 58.33333%; + } + + .row > .off-7-xlarge { + margin-left: 58.33333%; + } + + .row > .col-8-xlarge { + width: 66.66667%; + } + + .row > .off-8-xlarge { + margin-left: 66.66667%; + } + + .row > .col-9-xlarge { + width: 75%; + } + + .row > .off-9-xlarge { + margin-left: 75%; + } + + .row > .col-10-xlarge { + width: 83.33333%; + } + + .row > .off-10-xlarge { + margin-left: 83.33333%; + } + + .row > .col-11-xlarge { + width: 91.66667%; + } + + .row > .off-11-xlarge { + margin-left: 91.66667%; + } + + .row > .col-12-xlarge { + width: 100%; + } + + .row > .off-12-xlarge { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.5em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.5em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.5em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.5em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -1em; + } + + .row.gtr-50 > * { + padding: 0 0 0 1em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -1em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 1em; + } + + .row { + margin-top: 0; + margin-left: -2em; + } + + .row > * { + padding: 0 0 0 2em; + } + + .row.gtr-uniform { + margin-top: -2em; + } + + .row.gtr-uniform > * { + padding-top: 2em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-150 > * { + padding: 0 0 0 3em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 3em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -4em; + } + + .row.gtr-200 > * { + padding: 0 0 0 4em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -4em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 4em; + } + + } + + @media screen and (max-width: 1280px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-large { + order: -1; + } + + .row > .col-1-large { + width: 8.33333%; + } + + .row > .off-1-large { + margin-left: 8.33333%; + } + + .row > .col-2-large { + width: 16.66667%; + } + + .row > .off-2-large { + margin-left: 16.66667%; + } + + .row > .col-3-large { + width: 25%; + } + + .row > .off-3-large { + margin-left: 25%; + } + + .row > .col-4-large { + width: 33.33333%; + } + + .row > .off-4-large { + margin-left: 33.33333%; + } + + .row > .col-5-large { + width: 41.66667%; + } + + .row > .off-5-large { + margin-left: 41.66667%; + } + + .row > .col-6-large { + width: 50%; + } + + .row > .off-6-large { + margin-left: 50%; + } + + .row > .col-7-large { + width: 58.33333%; + } + + .row > .off-7-large { + margin-left: 58.33333%; + } + + .row > .col-8-large { + width: 66.66667%; + } + + .row > .off-8-large { + margin-left: 66.66667%; + } + + .row > .col-9-large { + width: 75%; + } + + .row > .off-9-large { + margin-left: 75%; + } + + .row > .col-10-large { + width: 83.33333%; + } + + .row > .off-10-large { + margin-left: 83.33333%; + } + + .row > .col-11-large { + width: 91.66667%; + } + + .row > .off-11-large { + margin-left: 91.66667%; + } + + .row > .col-12-large { + width: 100%; + } + + .row > .off-12-large { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + + } + + @media screen and (max-width: 980px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-medium { + order: -1; + } + + .row > .col-1-medium { + width: 8.33333%; + } + + .row > .off-1-medium { + margin-left: 8.33333%; + } + + .row > .col-2-medium { + width: 16.66667%; + } + + .row > .off-2-medium { + margin-left: 16.66667%; + } + + .row > .col-3-medium { + width: 25%; + } + + .row > .off-3-medium { + margin-left: 25%; + } + + .row > .col-4-medium { + width: 33.33333%; + } + + .row > .off-4-medium { + margin-left: 33.33333%; + } + + .row > .col-5-medium { + width: 41.66667%; + } + + .row > .off-5-medium { + margin-left: 41.66667%; + } + + .row > .col-6-medium { + width: 50%; + } + + .row > .off-6-medium { + margin-left: 50%; + } + + .row > .col-7-medium { + width: 58.33333%; + } + + .row > .off-7-medium { + margin-left: 58.33333%; + } + + .row > .col-8-medium { + width: 66.66667%; + } + + .row > .off-8-medium { + margin-left: 66.66667%; + } + + .row > .col-9-medium { + width: 75%; + } + + .row > .off-9-medium { + margin-left: 75%; + } + + .row > .col-10-medium { + width: 83.33333%; + } + + .row > .off-10-medium { + margin-left: 83.33333%; + } + + .row > .col-11-medium { + width: 91.66667%; + } + + .row > .off-11-medium { + margin-left: 91.66667%; + } + + .row > .col-12-medium { + width: 100%; + } + + .row > .off-12-medium { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.375em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.375em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.375em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.375em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.75em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.75em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.75em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.75em; + } + + .row { + margin-top: 0; + margin-left: -1.5em; + } + + .row > * { + padding: 0 0 0 1.5em; + } + + .row.gtr-uniform { + margin-top: -1.5em; + } + + .row.gtr-uniform > * { + padding-top: 1.5em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -2.25em; + } + + .row.gtr-150 > * { + padding: 0 0 0 2.25em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -2.25em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 2.25em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -3em; + } + + .row.gtr-200 > * { + padding: 0 0 0 3em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -3em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 3em; + } + + } + + @media screen and (max-width: 736px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-small { + order: -1; + } + + .row > .col-1-small { + width: 8.33333%; + } + + .row > .off-1-small { + margin-left: 8.33333%; + } + + .row > .col-2-small { + width: 16.66667%; + } + + .row > .off-2-small { + margin-left: 16.66667%; + } + + .row > .col-3-small { + width: 25%; + } + + .row > .off-3-small { + margin-left: 25%; + } + + .row > .col-4-small { + width: 33.33333%; + } + + .row > .off-4-small { + margin-left: 33.33333%; + } + + .row > .col-5-small { + width: 41.66667%; + } + + .row > .off-5-small { + margin-left: 41.66667%; + } + + .row > .col-6-small { + width: 50%; + } + + .row > .off-6-small { + margin-left: 50%; + } + + .row > .col-7-small { + width: 58.33333%; + } + + .row > .off-7-small { + margin-left: 58.33333%; + } + + .row > .col-8-small { + width: 66.66667%; + } + + .row > .off-8-small { + margin-left: 66.66667%; + } + + .row > .col-9-small { + width: 75%; + } + + .row > .off-9-small { + margin-left: 75%; + } + + .row > .col-10-small { + width: 83.33333%; + } + + .row > .off-10-small { + margin-left: 83.33333%; + } + + .row > .col-11-small { + width: 91.66667%; + } + + .row > .off-11-small { + margin-left: 91.66667%; + } + + .row > .col-12-small { + width: 100%; + } + + .row > .off-12-small { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.3125em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.3125em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.3125em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.3125em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.625em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.625em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.625em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.625em; + } + + .row { + margin-top: 0; + margin-left: -1.25em; + } + + .row > * { + padding: 0 0 0 1.25em; + } + + .row.gtr-uniform { + margin-top: -1.25em; + } + + .row.gtr-uniform > * { + padding-top: 1.25em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -1.875em; + } + + .row.gtr-150 > * { + padding: 0 0 0 1.875em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -1.875em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 1.875em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -2.5em; + } + + .row.gtr-200 > * { + padding: 0 0 0 2.5em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -2.5em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 2.5em; + } + + } + + @media screen and (max-width: 480px) { + + .row { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + align-items: stretch; + } + + .row > * { + box-sizing: border-box; + } + + .row.gtr-uniform > * > :last-child { + margin-bottom: 0; + } + + .row.aln-left { + justify-content: flex-start; + } + + .row.aln-center { + justify-content: center; + } + + .row.aln-right { + justify-content: flex-end; + } + + .row.aln-top { + align-items: flex-start; + } + + .row.aln-middle { + align-items: center; + } + + .row.aln-bottom { + align-items: flex-end; + } + + .row > .imp-xsmall { + order: -1; + } + + .row > .col-1-xsmall { + width: 8.33333%; + } + + .row > .off-1-xsmall { + margin-left: 8.33333%; + } + + .row > .col-2-xsmall { + width: 16.66667%; + } + + .row > .off-2-xsmall { + margin-left: 16.66667%; + } + + .row > .col-3-xsmall { + width: 25%; + } + + .row > .off-3-xsmall { + margin-left: 25%; + } + + .row > .col-4-xsmall { + width: 33.33333%; + } + + .row > .off-4-xsmall { + margin-left: 33.33333%; + } + + .row > .col-5-xsmall { + width: 41.66667%; + } + + .row > .off-5-xsmall { + margin-left: 41.66667%; + } + + .row > .col-6-xsmall { + width: 50%; + } + + .row > .off-6-xsmall { + margin-left: 50%; + } + + .row > .col-7-xsmall { + width: 58.33333%; + } + + .row > .off-7-xsmall { + margin-left: 58.33333%; + } + + .row > .col-8-xsmall { + width: 66.66667%; + } + + .row > .off-8-xsmall { + margin-left: 66.66667%; + } + + .row > .col-9-xsmall { + width: 75%; + } + + .row > .off-9-xsmall { + margin-left: 75%; + } + + .row > .col-10-xsmall { + width: 83.33333%; + } + + .row > .off-10-xsmall { + margin-left: 83.33333%; + } + + .row > .col-11-xsmall { + width: 91.66667%; + } + + .row > .off-11-xsmall { + margin-left: 91.66667%; + } + + .row > .col-12-xsmall { + width: 100%; + } + + .row > .off-12-xsmall { + margin-left: 100%; + } + + .row.gtr-0 { + margin-top: 0; + margin-left: 0em; + } + + .row.gtr-0 > * { + padding: 0 0 0 0em; + } + + .row.gtr-0.gtr-uniform { + margin-top: 0em; + } + + .row.gtr-0.gtr-uniform > * { + padding-top: 0em; + } + + .row.gtr-25 { + margin-top: 0; + margin-left: -0.3125em; + } + + .row.gtr-25 > * { + padding: 0 0 0 0.3125em; + } + + .row.gtr-25.gtr-uniform { + margin-top: -0.3125em; + } + + .row.gtr-25.gtr-uniform > * { + padding-top: 0.3125em; + } + + .row.gtr-50 { + margin-top: 0; + margin-left: -0.625em; + } + + .row.gtr-50 > * { + padding: 0 0 0 0.625em; + } + + .row.gtr-50.gtr-uniform { + margin-top: -0.625em; + } + + .row.gtr-50.gtr-uniform > * { + padding-top: 0.625em; + } + + .row { + margin-top: 0; + margin-left: -1.25em; + } + + .row > * { + padding: 0 0 0 1.25em; + } + + .row.gtr-uniform { + margin-top: -1.25em; + } + + .row.gtr-uniform > * { + padding-top: 1.25em; + } + + .row.gtr-150 { + margin-top: 0; + margin-left: -1.875em; + } + + .row.gtr-150 > * { + padding: 0 0 0 1.875em; + } + + .row.gtr-150.gtr-uniform { + margin-top: -1.875em; + } + + .row.gtr-150.gtr-uniform > * { + padding-top: 1.875em; + } + + .row.gtr-200 { + margin-top: 0; + margin-left: -2.5em; + } + + .row.gtr-200 > * { + padding: 0 0 0 2.5em; + } + + .row.gtr-200.gtr-uniform { + margin-top: -2.5em; + } + + .row.gtr-200.gtr-uniform > * { + padding-top: 2.5em; + } + + } + +/* Section/Article */ + + section.special, article.special { + text-align: center; + } + + header.major { + width: -moz-max-content; + width: -webkit-max-content; + width: -ms-max-content; + width: max-content; + margin-bottom: 2em; + } + + header.major > :first-child { + margin-bottom: 0; + width: calc(100% + 0.5em); + } + + header.major > :first-child:after { + content: ''; + background-color: #ffffff; + display: block; + height: 2px; + margin: 0.325em 0 0.5em 0; + width: 100%; + } + + header.major > p { + font-size: 0.7em; + font-weight: 600; + letter-spacing: 0.25em; + margin-bottom: 0; + text-transform: uppercase; + } + + body.is-ie header.major > :first-child:after { + max-width: 9em; + } + + body.is-ie header.major > h1:after { + max-width: 100% !important; + } + + @media screen and (max-width: 736px) { + + header.major > p br { + display: none; + } + + } + +/* Form */ + + form { + margin: 0 0 2em 0; + } + + form > :last-child { + margin-bottom: 0; + } + + form > .fields { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + width: calc(100% + 3em); + margin: -1.5em 0 2em -1.5em; + } + + form > .fields > .field { + -moz-flex-grow: 0; + -webkit-flex-grow: 0; + -ms-flex-grow: 0; + flex-grow: 0; + -moz-flex-shrink: 0; + -webkit-flex-shrink: 0; + -ms-flex-shrink: 0; + flex-shrink: 0; + padding: 1.5em 0 0 1.5em; + width: calc(100% - 1.5em); + } + + form > .fields > .field.half { + width: calc(50% - 0.75em); + } + + form > .fields > .field.third { + width: calc(100%/3 - 0.5em); + } + + form > .fields > .field.quarter { + width: calc(25% - 0.375em); + } + + @media screen and (max-width: 480px) { + + form > .fields { + width: calc(100% + 3em); + margin: -1.5em 0 2em -1.5em; + } + + form > .fields > .field { + padding: 1.5em 0 0 1.5em; + width: calc(100% - 1.5em); + } + + form > .fields > .field.half { + width: calc(100% - 1.5em); + } + + form > .fields > .field.third { + width: calc(100% - 1.5em); + } + + form > .fields > .field.quarter { + width: calc(100% - 1.5em); + } + + } + + label { + color: #ffffff; + display: block; + font-size: 0.8em; + font-weight: 600; + letter-spacing: 0.25em; + margin: 0 0 1em 0; + text-transform: uppercase; + } + + input[type="text"], + input[type="password"], + input[type="email"], + input[type="tel"], + input[type="search"], + input[type="url"], + select, + textarea { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; + background: rgba(212, 212, 255, 0.035); + border: none; + border-radius: 0; + color: inherit; + display: block; + outline: 0; + padding: 0 1em; + text-decoration: none; + width: 100%; + } + + input[type="text"]:invalid, + input[type="password"]:invalid, + input[type="email"]:invalid, + input[type="tel"]:invalid, + input[type="search"]:invalid, + input[type="url"]:invalid, + select:invalid, + textarea:invalid { + box-shadow: none; + } + + input[type="text"]:focus, + input[type="password"]:focus, + input[type="email"]:focus, + input[type="tel"]:focus, + input[type="search"]:focus, + input[type="url"]:focus, + select:focus, + textarea:focus { + border-color: #9bf1ff; + box-shadow: 0 0 0 2px #9bf1ff; + } + + select { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' preserveAspectRatio='none' viewBox='0 0 40 40'%3E%3Cpath d='M9.4,12.3l10.4,10.4l10.4-10.4c0.2-0.2,0.5-0.4,0.9-0.4c0.3,0,0.6,0.1,0.9,0.4l3.3,3.3c0.2,0.2,0.4,0.5,0.4,0.9 c0,0.4-0.1,0.6-0.4,0.9L20.7,31.9c-0.2,0.2-0.5,0.4-0.9,0.4c-0.3,0-0.6-0.1-0.9-0.4L4.3,17.3c-0.2-0.2-0.4-0.5-0.4-0.9 c0-0.4,0.1-0.6,0.4-0.9l3.3-3.3c0.2-0.2,0.5-0.4,0.9-0.4S9.1,12.1,9.4,12.3z' fill='rgba(212, 212, 255, 0.1)' /%3E%3C/svg%3E"); + background-size: 1.25rem; + background-repeat: no-repeat; + background-position: calc(100% - 1rem) center; + height: 2.75em; + padding-right: 2.75em; + text-overflow: ellipsis; + } + + select option { + color: #ffffff; + background: #242943; + } + + select:focus::-ms-value { + background-color: transparent; + } + + select::-ms-expand { + display: none; + } + + input[type="text"], + input[type="password"], + input[type="email"], + input[type="tel"], + input[type="search"], + input[type="url"], + select { + height: 2.75em; + } + + textarea { + padding: 0.75em 1em; + } + + input[type="checkbox"], + input[type="radio"] { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; + display: block; + float: left; + margin-right: -2em; + opacity: 0; + width: 1em; + z-index: -1; + } + + input[type="checkbox"] + label, + input[type="radio"] + label { + text-decoration: none; + color: #ffffff; + cursor: pointer; + display: inline-block; + font-weight: 300; + padding-left: 2.65em; + padding-right: 0.75em; + position: relative; + } + + input[type="checkbox"] + label:before, + input[type="radio"] + label:before { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + text-transform: none !important; + font-family: 'Font Awesome 5 Free'; + font-weight: 900; + } + + input[type="checkbox"] + label:before, + input[type="radio"] + label:before { + background: rgba(212, 212, 255, 0.035); + content: ''; + display: inline-block; + font-size: 0.8em; + height: 2.0625em; + left: 0; + letter-spacing: 0; + line-height: 2.0625em; + position: absolute; + text-align: center; + top: 0; + width: 2.0625em; + } + + input[type="checkbox"]:checked + label:before, + input[type="radio"]:checked + label:before { + background: #ffffff; + border-color: #9bf1ff; + content: '\f00c'; + color: #242943; + } + + input[type="checkbox"]:focus + label:before, + input[type="radio"]:focus + label:before { + box-shadow: 0 0 0 2px #9bf1ff; + } + + input[type="radio"] + label:before { + border-radius: 100%; + } + + ::-webkit-input-placeholder { + color: rgba(244, 244, 255, 0.2) !important; + opacity: 1.0; + } + + :-moz-placeholder { + color: rgba(244, 244, 255, 0.2) !important; + opacity: 1.0; + } + + ::-moz-placeholder { + color: rgba(244, 244, 255, 0.2) !important; + opacity: 1.0; + } + + :-ms-input-placeholder { + color: rgba(244, 244, 255, 0.2) !important; + opacity: 1.0; + } + +/* Box */ + + .box { + border: solid 1px rgba(212, 212, 255, 0.1); + margin-bottom: 2em; + padding: 1.5em; + } + + .box > :last-child, + .box > :last-child > :last-child, + .box > :last-child > :last-child > :last-child { + margin-bottom: 0; + } + + .box.alt { + border: 0; + border-radius: 0; + padding: 0; + } + +/* Icon */ + + .icon { + text-decoration: none; + border-bottom: none; + position: relative; + } + + .icon:before { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + text-transform: none !important; + font-family: 'Font Awesome 5 Free'; + font-weight: 400; + } + + .icon > .label { + display: none; + } + + .icon:before { + line-height: inherit; + } + + .icon.solid:before { + font-weight: 900; + } + + .icon.brands:before { + font-family: 'Font Awesome 5 Brands'; + } + + .icon.alt:before { + background-color: #ffffff; + border-radius: 100%; + color: #242943; + display: inline-block; + height: 2em; + line-height: 2em; + text-align: center; + width: 2em; + } + + a.icon.alt:before { + -moz-transition: background-color 0.2s ease-in-out; + -webkit-transition: background-color 0.2s ease-in-out; + -ms-transition: background-color 0.2s ease-in-out; + transition: background-color 0.2s ease-in-out; + } + + a.icon.alt:hover:before { + background-color: #6fc3df; + } + + a.icon.alt:active:before { + background-color: #37a6cb; + } + +/* Image */ + + .image { + border: 0; + display: inline-block; + position: relative; + } + + .image img { + display: block; + } + + .image.left, .image.right { + max-width: 30%; + } + + .image.left img, .image.right img { + width: 100%; + } + + .image.left { + float: left; + margin: 0 1.5em 1.25em 0; + top: 0.25em; + } + + .image.right { + float: right; + margin: 0 0 1.25em 1.5em; + top: 0.25em; + } + + .image.fit { + display: block; + margin: 0 0 2em 0; + width: 100%; + } + + .image.fit img { + width: 100%; + } + + .image.main { + display: block; + margin: 2.5em 0; + width: 100%; + } + + .image.main img { + width: 100%; + } + + @media screen and (max-width: 736px) { + + .image.main { + margin: 1.5em 0; + } + + } + +/* List */ + + ol { + list-style: decimal; + margin: 0 0 2em 0; + padding-left: 1.25em; + } + + ol li { + padding-left: 0.25em; + } + + ul { + list-style: disc; + margin: 0 0 2em 0; + padding-left: 1em; + } + + ul li { + padding-left: 0.5em; + } + + ul.alt { + list-style: none; + padding-left: 0; + } + + ul.alt li { + border-top: solid 1px rgba(212, 212, 255, 0.1); + padding: 0.5em 0; + } + + ul.alt li:first-child { + border-top: 0; + padding-top: 0; + } + + dl { + margin: 0 0 2em 0; + } + + dl dt { + display: block; + font-weight: 600; + margin: 0 0 1em 0; + } + + dl dd { + margin-left: 2em; + } + +/* Actions */ + + ul.actions { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + cursor: default; + list-style: none; + margin-left: -1em; + padding-left: 0; + } + + ul.actions li { + padding: 0 0 0 1em; + vertical-align: middle; + } + + ul.actions.special { + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + width: 100%; + margin-left: 0; + } + + ul.actions.special li:first-child { + padding-left: 0; + } + + ul.actions.stacked { + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + margin-left: 0; + } + + ul.actions.stacked li { + padding: 1.3em 0 0 0; + } + + ul.actions.stacked li:first-child { + padding-top: 0; + } + + ul.actions.fit { + width: calc(100% + 1em); + } + + ul.actions.fit li { + -moz-flex-grow: 1; + -webkit-flex-grow: 1; + -ms-flex-grow: 1; + flex-grow: 1; + -moz-flex-shrink: 1; + -webkit-flex-shrink: 1; + -ms-flex-shrink: 1; + flex-shrink: 1; + width: 100%; + } + + ul.actions.fit li > * { + width: 100%; + } + + ul.actions.fit.stacked { + width: 100%; + } + +/* Icons */ + + ul.icons { + cursor: default; + list-style: none; + padding-left: 0; + } + + ul.icons li { + display: inline-block; + padding: 0 1em 0 0; + } + + ul.icons li:last-child { + padding-right: 0; + } + + @media screen and (max-width: 736px) { + + ul.icons li { + padding: 0 0.75em 0 0; + } + + } + +/* Pagination */ + + ul.pagination { + cursor: default; + list-style: none; + padding-left: 0; + } + + ul.pagination li { + display: inline-block; + padding-left: 0; + vertical-align: middle; + } + + ul.pagination li > .page { + -moz-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + -webkit-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + -ms-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + border-bottom: 0; + display: inline-block; + font-size: 0.8em; + font-weight: 600; + height: 1.5em; + line-height: 1.5em; + margin: 0 0.125em; + min-width: 1.5em; + padding: 0 0.5em; + text-align: center; + } + + ul.pagination li > .page.active { + background-color: #ffffff; + color: #242943; + } + + ul.pagination li > .page.active:hover { + background-color: #9bf1ff; + color: #242943 !important; + } + + ul.pagination li > .page.active:active { + background-color: #53e3fb; + } + + ul.pagination li:first-child { + padding-right: 0.75em; + } + + ul.pagination li:last-child { + padding-left: 0.75em; + } + + @media screen and (max-width: 480px) { + + ul.pagination li:nth-child(n+2):nth-last-child(n+2) { + display: none; + } + + ul.pagination li:first-child { + padding-right: 0; + } + + } + +/* Table */ + + .table-wrapper { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + } + + table { + margin: 0 0 2em 0; + width: 100%; + } + + table tbody tr { + border: solid 1px rgba(212, 212, 255, 0.1); + border-left: 0; + border-right: 0; + } + + table tbody tr:nth-child(2n + 1) { + background-color: rgba(212, 212, 255, 0.035); + } + + table td { + padding: 0.75em 0.75em; + } + + table th { + color: #ffffff; + font-size: 0.9em; + font-weight: 600; + padding: 0 0.75em 0.75em 0.75em; + text-align: left; + } + + table thead { + border-bottom: solid 2px rgba(212, 212, 255, 0.1); + } + + table tfoot { + border-top: solid 2px rgba(212, 212, 255, 0.1); + } + + table.alt { + border-collapse: separate; + } + + table.alt tbody tr td { + border: solid 1px rgba(212, 212, 255, 0.1); + border-left-width: 0; + border-top-width: 0; + } + + table.alt tbody tr td:first-child { + border-left-width: 1px; + } + + table.alt tbody tr:first-child td { + border-top-width: 1px; + } + + table.alt thead { + border-bottom: 0; + } + + table.alt tfoot { + border-top: 0; + } + +/* Button */ + + input[type="submit"], + input[type="reset"], + input[type="button"], + button, + .button { + -moz-appearance: none; + -webkit-appearance: none; + -ms-appearance: none; + appearance: none; + -moz-transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, color 0.2s ease-in-out; + -webkit-transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, color 0.2s ease-in-out; + -ms-transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, color 0.2s ease-in-out; + transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, color 0.2s ease-in-out; + background-color: transparent; + border: 0; + border-radius: 0; + box-shadow: inset 0 0 0 2px #ffffff; + color: #ffffff; + cursor: pointer; + display: inline-block; + font-size: 0.8em; + font-weight: 600; + height: 3.5em; + letter-spacing: 0.25em; + line-height: 3.5em; + padding: 0 1.75em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + } + + input[type="submit"]:hover, input[type="submit"]:active, + input[type="reset"]:hover, + input[type="reset"]:active, + input[type="button"]:hover, + input[type="button"]:active, + button:hover, + button:active, + .button:hover, + .button:active { + box-shadow: inset 0 0 0 2px #9bf1ff; + color: #9bf1ff; + } + + input[type="submit"]:active, + input[type="reset"]:active, + input[type="button"]:active, + button:active, + .button:active { + background-color: rgba(155, 241, 255, 0.1); + box-shadow: inset 0 0 0 2px #53e3fb; + color: #53e3fb; + } + + input[type="submit"].icon:before, + input[type="reset"].icon:before, + input[type="button"].icon:before, + button.icon:before, + .button.icon:before { + margin-right: 0.5em; + } + + input[type="submit"].fit, + input[type="reset"].fit, + input[type="button"].fit, + button.fit, + .button.fit { + width: 100%; + } + + input[type="submit"].small, + input[type="reset"].small, + input[type="button"].small, + button.small, + .button.small { + font-size: 0.6em; + } + + input[type="submit"].large, + input[type="reset"].large, + input[type="button"].large, + button.large, + .button.large { + font-size: 1.25em; + height: 3em; + line-height: 3em; + } + + input[type="submit"].next, + input[type="reset"].next, + input[type="button"].next, + button.next, + .button.next { + padding-right: 4.5em; + position: relative; + } + + input[type="submit"].next:before, input[type="submit"].next:after, + input[type="reset"].next:before, + input[type="reset"].next:after, + input[type="button"].next:before, + input[type="button"].next:after, + button.next:before, + button.next:after, + .button.next:before, + .button.next:after { + -moz-transition: opacity 0.2s ease-in-out; + -webkit-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; + background-position: center right; + background-repeat: no-repeat; + background-size: 36px 24px; + content: ''; + display: block; + height: 100%; + position: absolute; + right: 1.5em; + top: 0; + vertical-align: middle; + width: 36px; + } + + input[type="submit"].next:before, + input[type="reset"].next:before, + input[type="button"].next:before, + button.next:before, + .button.next:before { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='36px' height='24px' viewBox='0 0 36 24' zoomAndPan='disable'%3E%3Cstyle%3Eline %7B stroke: %23ffffff%3B stroke-width: 2px%3B %7D%3C/style%3E%3Cline x1='0' y1='12' x2='34' y2='12' /%3E%3Cline x1='25' y1='4' x2='34' y2='12.5' /%3E%3Cline x1='25' y1='20' x2='34' y2='11.5' /%3E%3C/svg%3E"); + } + + input[type="submit"].next:after, + input[type="reset"].next:after, + input[type="button"].next:after, + button.next:after, + .button.next:after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='36px' height='24px' viewBox='0 0 36 24' zoomAndPan='disable'%3E%3Cstyle%3Eline %7B stroke: %239bf1ff%3B stroke-width: 2px%3B %7D%3C/style%3E%3Cline x1='0' y1='12' x2='34' y2='12' /%3E%3Cline x1='25' y1='4' x2='34' y2='12.5' /%3E%3Cline x1='25' y1='20' x2='34' y2='11.5' /%3E%3C/svg%3E"); + opacity: 0; + z-index: 1; + } + + input[type="submit"].next:hover:after, input[type="submit"].next:active:after, + input[type="reset"].next:hover:after, + input[type="reset"].next:active:after, + input[type="button"].next:hover:after, + input[type="button"].next:active:after, + button.next:hover:after, + button.next:active:after, + .button.next:hover:after, + .button.next:active:after { + opacity: 1; + } + + @media screen and (max-width: 1280px) { + + input[type="submit"].next, + input[type="reset"].next, + input[type="button"].next, + button.next, + .button.next { + padding-right: 5em; + } + + } + + input[type="submit"].primary, + input[type="reset"].primary, + input[type="button"].primary, + button.primary, + .button.primary { + background-color: #ffffff; + box-shadow: none; + color: #242943; + } + + input[type="submit"].primary:hover, input[type="submit"].primary:active, + input[type="reset"].primary:hover, + input[type="reset"].primary:active, + input[type="button"].primary:hover, + input[type="button"].primary:active, + button.primary:hover, + button.primary:active, + .button.primary:hover, + .button.primary:active { + background-color: #9bf1ff; + color: #242943 !important; + } + + input[type="submit"].primary:active, + input[type="reset"].primary:active, + input[type="button"].primary:active, + button.primary:active, + .button.primary:active { + background-color: #53e3fb; + } + + input[type="submit"].disabled, input[type="submit"]:disabled, + input[type="reset"].disabled, + input[type="reset"]:disabled, + input[type="button"].disabled, + input[type="button"]:disabled, + button.disabled, + button:disabled, + .button.disabled, + .button:disabled { + pointer-events: none; + cursor: default; + opacity: 0.25; + } + +/* Tiles */ + + .tiles { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + border-top: 0 !important; + } + + .tiles + * { + border-top: 0 !important; + } + + .tiles article { + -moz-align-items: center; + -webkit-align-items: center; + -ms-align-items: center; + align-items: center; + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-transition: -moz-transform 0.25s ease, opacity 0.25s ease, -moz-filter 1s ease, -webkit-filter 1s ease; + -webkit-transition: -webkit-transform 0.25s ease, opacity 0.25s ease, -webkit-filter 1s ease, -webkit-filter 1s ease; + -ms-transition: -ms-transform 0.25s ease, opacity 0.25s ease, -ms-filter 1s ease, -webkit-filter 1s ease; + transition: transform 0.25s ease, opacity 0.25s ease, filter 1s ease, -webkit-filter 1s ease; + padding: 4em 4em 2em 4em ; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + cursor: default; + height: 40vh; + max-height: 40em; + min-height: 23em; + overflow: hidden; + position: relative; + width: 40%; + } + + .tiles article .image { + display: none; + } + + .tiles article header { + position: relative; + z-index: 3; + } + + .tiles article h3 { + font-size: 1.75em; + } + + .tiles article h3 a:hover { + color: inherit !important; + } + + .tiles article .link.primary { + border: 0; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: 4; + } + + .tiles article:before { + -moz-transition: opacity 0.5s ease; + -webkit-transition: opacity 0.5s ease; + -ms-transition: opacity 0.5s ease; + transition: opacity 0.5s ease; + bottom: 0; + content: ''; + display: block; + height: 100%; + left: 0; + opacity: 0.85; + position: absolute; + width: 100%; + z-index: 2; + } + + .tiles article:after { + background-color: rgba(36, 41, 67, 0.25); + content: ''; + display: block; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: 1; + } + + .tiles article:hover:before { + opacity: 0; + } + + .tiles article.is-transitioning { + -moz-transform: scale(0.95); + -webkit-transform: scale(0.95); + -ms-transform: scale(0.95); + transform: scale(0.95); + -moz-filter: blur(0.5em); + -webkit-filter: blur(0.5em); + -ms-filter: blur(0.5em); + filter: blur(0.5em); + opacity: 0; + } + + .tiles article:nth-child(4n - 1), .tiles article:nth-child(4n - 2) { + width: 60%; + } + + .tiles article:nth-child(6n - 5):before { + background-color: #6fc3df; + } + + .tiles article:nth-child(6n - 4):before { + background-color: #8d82c4; + } + + .tiles article:nth-child(6n - 3):before { + background-color: #ec8d81; + } + + .tiles article:nth-child(6n - 2):before { + background-color: #e7b788; + } + + .tiles article:nth-child(6n - 1):before { + background-color: #8ea9e8; + } + + .tiles article:nth-child(6n):before { + background-color: #87c5a4; + } + + @media screen and (max-width: 1280px) { + + .tiles article { + padding: 4em 3em 2em 3em ; + height: 30vh; + max-height: 30em; + min-height: 20em; + } + + } + + @media screen and (max-width: 980px) { + + .tiles article { + width: 50% !important; + } + + } + + @media screen and (max-width: 736px) { + + .tiles article { + padding: 3em 1.5em 1em 1.5em ; + height: 16em; + max-height: none; + min-height: 0; + } + + .tiles article h3 { + font-size: 1.5em; + } + + } + + @media screen and (max-width: 480px) { + + .tiles { + display: block; + } + + .tiles article { + height: 20em; + width: 100% !important; + } + + } + +/* Contact Method */ + + .contact-method { + margin: 0 0 2em 0; + padding-left: 3.25em; + position: relative; + } + + .contact-method .icon { + left: 0; + position: absolute; + top: 0; + } + + .contact-method h3 { + margin: 0 0 0.5em 0; + } + +/* Spotlights */ + + .spotlights { + border-top: 0 !important; + } + + .spotlights + * { + border-top: 0 !important; + } + + .spotlights > section { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-direction: row; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + background-color: #2e3450; + } + + .spotlights > section > .image { + background-position: center center; + background-size: cover; + border-radius: 0; + display: block; + position: relative; + width: 30%; + } + + .spotlights > section > .image img { + border-radius: 0; + display: block; + width: 100%; + } + + .spotlights > section > .image:before { + background: rgba(36, 41, 67, 0.9); + content: ''; + display: block; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + width: 100%; + } + + .spotlights > section > .content { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-flex-direction: column; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + -moz-align-items: center; + -webkit-align-items: center; + -ms-align-items: center; + align-items: center; + padding: 2em 3em 0.1em 3em ; + width: 70%; + } + + .spotlights > section > .content > .inner { + margin: 0 auto; + max-width: 100%; + width: 65em; + } + + .spotlights > section:nth-child(2n) { + -moz-flex-direction: row-reverse; + -webkit-flex-direction: row-reverse; + -ms-flex-direction: row-reverse; + flex-direction: row-reverse; + background-color: #333856; + } + + .spotlights > section:nth-child(2n) > .content { + -moz-align-items: -moz-flex-end; + -webkit-align-items: -webkit-flex-end; + -ms-align-items: -ms-flex-end; + align-items: flex-end; + } + + @media screen and (max-width: 1680px) { + + .spotlights > section > .image { + width: 40%; + } + + .spotlights > section > .content { + width: 60%; + } + + } + + @media screen and (max-width: 1280px) { + + .spotlights > section > .image { + width: 45%; + } + + .spotlights > section > .content { + width: 55%; + } + + } + + @media screen and (max-width: 980px) { + + .spotlights > section { + display: block; + } + + .spotlights > section > .image { + width: 100%; + } + + .spotlights > section > .content { + padding: 4em 3em 2em 3em ; + width: 100%; + } + + } + + @media screen and (max-width: 736px) { + + .spotlights > section > .content { + padding: 3em 1.5em 1em 1.5em ; + } + + } + +/* Header */ + + @-moz-keyframes reveal-header { + 0% { + top: -4em; + opacity: 0; + } + + 100% { + top: 0; + opacity: 1; + } + } + + @-webkit-keyframes reveal-header { + 0% { + top: -4em; + opacity: 0; + } + + 100% { + top: 0; + opacity: 1; + } + } + + @-ms-keyframes reveal-header { + 0% { + top: -4em; + opacity: 0; + } + + 100% { + top: 0; + opacity: 1; + } + } + + @keyframes reveal-header { + 0% { + top: -4em; + opacity: 0; + } + + 100% { + top: 0; + opacity: 1; + } + } + + #header { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + background-color: #2a2f4a; + box-shadow: 0 0 0.25em 0 rgba(0, 0, 0, 0.15); + cursor: default; + font-weight: 600; + height: 3.25em; + left: 0; + letter-spacing: 0.25em; + line-height: 3.25em; + margin: 0; + position: fixed; + text-transform: uppercase; + top: 0; + width: 100%; + z-index: 10000; + } + + #header .logo { + border: 0; + display: inline-block; + font-size: 0.8em; + height: inherit; + line-height: inherit; + padding: 0 1.5em; + } + + #header .logo strong { + -moz-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + -webkit-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + -ms-transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + background-color: #ffffff; + color: #242943; + display: inline-block; + line-height: 1.65em; + margin-right: 0.325em; + padding: 0 0.125em 0 0.375em; + } + + #header .logo:hover strong { + background-color: #9bf1ff; + } + + #header .logo:active strong { + background-color: #53e3fb; + } + + #header nav { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-justify-content: -moz-flex-end; + -webkit-justify-content: -webkit-flex-end; + -ms-justify-content: -ms-flex-end; + justify-content: flex-end; + -moz-flex-grow: 1; + -webkit-flex-grow: 1; + -ms-flex-grow: 1; + flex-grow: 1; + height: inherit; + line-height: inherit; + } + + #header nav a { + border: 0; + display: block; + font-size: 0.8em; + height: inherit; + line-height: inherit; + padding: 0 0.75em; + position: relative; + vertical-align: middle; + } + + #header nav a:last-child { + padding-right: 1.5em; + } + + #header nav a[href="#menu"] { + padding-right: 3.325em !important; + } + + #header nav a[href="#menu"]:before, #header nav a[href="#menu"]:after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='32' viewBox='0 0 24 32' preserveAspectRatio='none'%3E%3Cstyle%3Eline %7B stroke-width: 2px%3B stroke: %23ffffff%3B %7D%3C/style%3E%3Cline x1='0' y1='11' x2='24' y2='11' /%3E%3Cline x1='0' y1='21' x2='24' y2='21' /%3E%3Cline x1='0' y1='16' x2='24' y2='16' /%3E%3C/svg%3E"); + background-position: center; + background-repeat: no-repeat; + background-size: 24px 32px; + content: ''; + display: block; + height: 100%; + position: absolute; + right: 1.5em; + top: 0; + vertical-align: middle; + width: 24px; + } + + #header nav a[href="#menu"]:after { + -moz-transition: opacity 0.2s ease-in-out; + -webkit-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='32' viewBox='0 0 24 32' preserveAspectRatio='none'%3E%3Cstyle%3Eline %7B stroke-width: 2px%3B stroke: %239bf1ff%3B %7D%3C/style%3E%3Cline x1='0' y1='11' x2='24' y2='11' /%3E%3Cline x1='0' y1='21' x2='24' y2='21' /%3E%3Cline x1='0' y1='16' x2='24' y2='16' /%3E%3C/svg%3E"); + opacity: 0; + z-index: 1; + } + + #header nav a[href="#menu"]:hover:after, #header nav a[href="#menu"]:active:after { + opacity: 1; + } + + #header nav a[href="#menu"]:last-child { + padding-right: 3.875em !important; + } + + #header nav a[href="#menu"]:last-child:before, #header nav a[href="#menu"]:last-child:after { + right: 2em; + } + + #header.reveal { + -moz-animation: reveal-header 0.35s ease; + -webkit-animation: reveal-header 0.35s ease; + -ms-animation: reveal-header 0.35s ease; + animation: reveal-header 0.35s ease; + } + + #header.alt { + -moz-transition: opacity 2.5s ease; + -webkit-transition: opacity 2.5s ease; + -ms-transition: opacity 2.5s ease; + transition: opacity 2.5s ease; + -moz-transition-delay: 0.75s; + -webkit-transition-delay: 0.75s; + -ms-transition-delay: 0.75s; + transition-delay: 0.75s; + -moz-animation: none; + -webkit-animation: none; + -ms-animation: none; + animation: none; + background-color: transparent; + box-shadow: none; + position: absolute; + } + + #header.alt.style1 .logo strong { + color: #6fc3df; + } + + #header.alt.style2 .logo strong { + color: #8d82c4; + } + + #header.alt.style3 .logo strong { + color: #ec8d81; + } + + #header.alt.style4 .logo strong { + color: #e7b788; + } + + #header.alt.style5 .logo strong { + color: #8ea9e8; + } + + #header.alt.style6 .logo strong { + color: #87c5a4; + } + + body.is-preload #header.alt { + opacity: 0; + } + + @media screen and (max-width: 1680px) { + + #header nav a[href="#menu"] { + padding-right: 3.75em !important; + } + + #header nav a[href="#menu"]:last-child { + padding-right: 4.25em !important; + } + + } + + @media screen and (max-width: 1280px) { + + #header nav a[href="#menu"] { + padding-right: 4em !important; + } + + #header nav a[href="#menu"]:last-child { + padding-right: 4.5em !important; + } + + } + + @media screen and (max-width: 736px) { + + #header { + height: 2.75em; + line-height: 2.75em; + } + + #header .logo { + padding: 0 1em; + } + + #header nav a { + padding: 0 0.5em; + } + + #header nav a:last-child { + padding-right: 1em; + } + + #header nav a[href="#menu"] { + padding-right: 3.25em !important; + } + + #header nav a[href="#menu"]:before, #header nav a[href="#menu"]:after { + right: 0.75em; + } + + #header nav a[href="#menu"]:last-child { + padding-right: 4em !important; + } + + #header nav a[href="#menu"]:last-child:before, #header nav a[href="#menu"]:last-child:after { + right: 1.5em; + } + + } + + @media screen and (max-width: 480px) { + + #header .logo span { + display: none; + } + + #header nav a[href="#menu"] { + overflow: hidden; + padding-right: 0 !important; + text-indent: 5em; + white-space: nowrap; + width: 5em; + } + + #header nav a[href="#menu"]:before, #header nav a[href="#menu"]:after { + right: 0; + width: inherit; + } + + #header nav a[href="#menu"]:last-child:before, #header nav a[href="#menu"]:last-child:after { + width: 4em; + right: 0; + } + + } + +/* Banner */ + + #banner { + -moz-align-items: center; + -webkit-align-items: center; + -ms-align-items: center; + align-items: center; + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + padding: 12em 0 2em 0 ; + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + border-bottom: 0 !important; + cursor: default; + height: 60vh; + margin-bottom: -3.25em; + max-height: 32em; + min-height: 22em; + position: relative; + top: -3.25em; + } + + #banner:after { + -moz-transition: opacity 2.5s ease; + -webkit-transition: opacity 2.5s ease; + -ms-transition: opacity 2.5s ease; + transition: opacity 2.5s ease; + -moz-transition-delay: 0.75s; + -webkit-transition-delay: 0.75s; + -ms-transition-delay: 0.75s; + transition-delay: 0.75s; + pointer-events: none; + background-color: transparent; + content: ''; + display: block; + height: 100%; + left: 0; + opacity: 0.85; + position: absolute; + top: 0; + width: 100%; + z-index: 1; + } + + #banner h1 { + font-size: 2.5em; + } + + #banner > .inner { + -moz-transition: opacity 1.5s ease, -moz-transform 0.5s ease-out, -moz-filter 0.5s ease, -webkit-filter 0.5s ease; + -webkit-transition: opacity 1.5s ease, -webkit-transform 0.5s ease-out, -webkit-filter 0.5s ease, -webkit-filter 0.5s ease; + -ms-transition: opacity 1.5s ease, -ms-transform 0.5s ease-out, -ms-filter 0.5s ease, -webkit-filter 0.5s ease; + transition: opacity 1.5s ease, transform 0.5s ease-out, filter 0.5s ease, -webkit-filter 0.5s ease; + padding: 0 !important; + position: relative; + z-index: 2; + } + + #banner > .inner .image { + display: none; + } + + #banner > .inner header { + width: auto; + } + + #banner > .inner header > :first-child { + width: auto; + } + + #banner > .inner header > :first-child:after { + max-width: 100%; + } + + #banner > .inner .content { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-align-items: center; + -webkit-align-items: center; + -ms-align-items: center; + align-items: center; + margin: 0 0 2em 0; + } + + #banner > .inner .content > * { + margin-right: 1.5em; + margin-bottom: 0; + } + + #banner > .inner .content > :last-child { + margin-right: 0; + } + + #banner > .inner .content p { + font-size: 0.7em; + font-weight: 600; + letter-spacing: 0.25em; + text-transform: uppercase; + } + + #banner.major { + height: 75vh; + min-height: 30em; + max-height: 50em; + } + + #banner.major.alt { + opacity: 0.75; + } + + #banner.style1:after { + background-color: #6fc3df; + } + + #banner.style2:after { + background-color: #8d82c4; + } + + #banner.style3:after { + background-color: #ec8d81; + } + + #banner.style4:after { + background-color: #e7b788; + } + + #banner.style5:after { + background-color: #8ea9e8; + } + + #banner.style6:after { + background-color: #87c5a4; + } + + body.is-preload #banner:after { + opacity: 1.0; + } + + body.is-preload #banner > .inner { + -moz-filter: blur(0.125em); + -webkit-filter: blur(0.125em); + -ms-filter: blur(0.125em); + filter: blur(0.125em); + -moz-transform: translateX(-0.5em); + -webkit-transform: translateX(-0.5em); + -ms-transform: translateX(-0.5em); + transform: translateX(-0.5em); + opacity: 0; + } + + @media screen and (max-width: 1280px) { + + #banner { + background-attachment: scroll; + } + + } + + @media screen and (max-width: 736px) { + + #banner { + padding: 5em 0 1em 0 ; + height: auto; + margin-bottom: -2.75em; + max-height: none; + min-height: 0; + top: -2.75em; + } + + #banner h1 { + font-size: 2em; + } + + #banner > .inner .content { + display: block; + } + + #banner > .inner .content > * { + margin-right: 0; + margin-bottom: 2em; + } + + #banner.major { + height: 75vh; + min-height: 0; + max-height: none; + } + + } + + @media screen and (max-width: 480px) { + + #banner { + padding: 6em 0 2em 0 ; + } + + #banner > .inner .content p br { + display: none; + } + + #banner.major { + padding: 8em 0 4em 0 ; + } + + } + +/* Main */ + + #main { + background-color: #2a2f4a; + } + + #main > * { + border-top: solid 1px rgba(212, 212, 255, 0.1); + } + + #main > *:first-child { + border-top: 0; + } + + #main > * > .inner { + padding: 4em 0 2em 0 ; + margin: 0 auto; + max-width: 65em; + width: calc(100% - 6em); + } + + @media screen and (max-width: 736px) { + + #main > * > .inner { + padding: 3em 0 1em 0 ; + width: calc(100% - 3em); + } + + } + + #main.alt { + background-color: transparent; + border-bottom: solid 1px rgba(212, 212, 255, 0.1); + } + +/* Contact */ + + #contact { + border-bottom: solid 1px rgba(212, 212, 255, 0.1); + overflow-x: hidden; + } + + #contact > .inner { + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + padding: 0 !important; + } + + #contact > .inner > :nth-child(2n - 1) { + padding: 4em 3em 2em 0 ; + border-right: solid 1px rgba(212, 212, 255, 0.1); + width: 60%; + } + + #contact > .inner > :nth-child(2n) { + padding-left: 3em; + width: 40%; + } + + #contact > .inner > .split { + padding: 0; + } + + #contact > .inner > .split > * { + padding: 3em 0 1em 3em ; + position: relative; + } + + #contact > .inner > .split > *:before { + border-top: solid 1px rgba(212, 212, 255, 0.1); + content: ''; + display: block; + margin-left: -3em; + position: absolute; + top: 0; + width: calc(100vw + 3em); + } + + #contact > .inner > .split > :first-child:before { + display: none; + } + + @media screen and (max-width: 980px) { + + #contact > .inner { + display: block; + } + + #contact > .inner > :nth-child(2n - 1) { + padding: 4em 0 2em 0 ; + border-right: 0; + width: 100%; + } + + #contact > .inner > :nth-child(2n) { + padding-left: 0; + width: 100%; + } + + #contact > .inner > .split > * { + padding: 3em 0 1em 0 ; + } + + #contact > .inner > .split > :first-child:before { + display: block; + } + + } + + @media screen and (max-width: 736px) { + + #contact > .inner > :nth-child(2n - 1) { + padding: 3em 0 1em 0 ; + } + + } + +/* Footer */ + + #footer .copyright { + font-size: 0.8em; + list-style: none; + padding-left: 0; + } + + #footer .copyright li { + border-left: solid 1px rgba(212, 212, 255, 0.1); + color: rgba(244, 244, 255, 0.2); + display: inline-block; + line-height: 1; + margin-left: 1em; + padding-left: 1em; + } + + #footer .copyright li:first-child { + border-left: 0; + margin-left: 0; + padding-left: 0; + } + + @media screen and (max-width: 480px) { + + #footer .copyright li { + display: block; + border-left: 0; + margin-left: 0; + padding-left: 0; + line-height: inherit; + } + + } + +/* Wrapper */ + + #wrapper { + -moz-transition: -moz-filter 0.35s ease, -webkit-filter 0.35s ease, opacity 0.375s ease-out; + -webkit-transition: -webkit-filter 0.35s ease, -webkit-filter 0.35s ease, opacity 0.375s ease-out; + -ms-transition: -ms-filter 0.35s ease, -webkit-filter 0.35s ease, opacity 0.375s ease-out; + transition: filter 0.35s ease, -webkit-filter 0.35s ease, opacity 0.375s ease-out; + padding-top: 3.25em; + height: 100vh; + } + + #wrapper.is-transitioning { + opacity: 0; + } + + #wrapper > * > .inner { + padding: 4em 0 2em 0 ; + margin: 0 auto; + max-width: 65em; + width: calc(100% - 6em); + } + + @media screen and (max-width: 736px) { + + #wrapper > * > .inner { + padding: 3em 0 1em 0 ; + width: calc(100% - 3em); + } + + } + + @media screen and (max-width: 736px) { + + #wrapper { + padding-top: 2.75em; + } + + } + +/* Menu */ + + #menu { + -moz-transition: -moz-transform 0.35s ease, opacity 0.35s ease, visibility 0.35s; + -webkit-transition: -webkit-transform 0.35s ease, opacity 0.35s ease, visibility 0.35s; + -ms-transition: -ms-transform 0.35s ease, opacity 0.35s ease, visibility 0.35s; + transition: transform 0.35s ease, opacity 0.35s ease, visibility 0.35s; + -moz-align-items: center; + -webkit-align-items: center; + -ms-align-items: center; + align-items: center; + display: -moz-flex; + display: -webkit-flex; + display: -ms-flex; + display: flex; + -moz-justify-content: center; + -webkit-justify-content: center; + -ms-justify-content: center; + justify-content: center; + pointer-events: none; + background: rgb(0 0 0 / 50%);; + box-shadow: none; + height: 100%; + left: 0; + opacity: 0; + overflow: hidden; + padding: 3em 2em; + position: fixed; + top: 0; + visibility: hidden; + width: 100%; + z-index: 10002; + } + + #menu .inner { + -moz-transition: -moz-transform 0.35s ease-out, opacity 0.35s ease, visibility 0.35s; + -webkit-transition: -webkit-transform 0.35s ease-out, opacity 0.35s ease, visibility 0.35s; + -ms-transition: -ms-transform 0.35s ease-out, opacity 0.35s ease, visibility 0.35s; + transition: transform 0.35s ease-out, opacity 0.35s ease, visibility 0.35s; + -moz-transform: rotateX(20deg); + -webkit-transform: rotateX(20deg); + -ms-transform: rotateX(20deg); + transform: rotateX(20deg); + -webkit-overflow-scrolling: touch; + max-width: 100%; + max-height: 100vh; + opacity: 0; + overflow: auto; + text-align: center; + visibility: hidden; + width: 18em; + } + + #menu .inner > :first-child { + margin-top: 2em; + } + + #menu .inner > :last-child { + margin-bottom: 3em; + } + + #menu ul { + margin: 0 0 1em 0; + } + + #menu ul.links { + list-style: none; + padding: 0; + } + + #menu ul.links > li { + padding: 0; + } + + #menu ul.links > li > a:not(.button) { + border: 0; + border-top: solid 1px rgba(212, 212, 255, 0.1); + display: block; + font-size: 0.8em; + font-weight: 600; + letter-spacing: 0.25em; + line-height: 4em; + text-decoration: none; + text-transform: uppercase; + } + + #menu ul.links > li > .button { + display: block; + margin: 0.5em 0 0 0; + } + + #menu ul.links > li:first-child > a:not(.button) { + border-top: 0 !important; + } + + #menu .close { + -moz-transition: color 0.2s ease-in-out; + -webkit-transition: color 0.2s ease-in-out; + -ms-transition: color 0.2s ease-in-out; + transition: color 0.2s ease-in-out; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + border: 0; + cursor: pointer; + display: block; + height: 4em; + line-height: 4em; + overflow: hidden; + padding-right: 1.25em; + position: absolute; + right: 0; + text-align: right; + text-indent: 8em; + top: 0; + vertical-align: middle; + white-space: nowrap; + width: 8em; + } + + #menu .close:before, #menu .close:after { + -moz-transition: opacity 0.2s ease-in-out; + -webkit-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; + background-position: center; + background-repeat: no-repeat; + content: ''; + display: block; + height: 4em; + position: absolute; + right: 0; + top: 0; + width: 4em; + } + + #menu .close:before { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='20px' height='20px' viewBox='0 0 20 20' zoomAndPan='disable'%3E%3Cstyle%3Eline %7B stroke: %23ffffff%3B stroke-width: 2%3B %7D%3C/style%3E%3Cline x1='0' y1='0' x2='20' y2='20' /%3E%3Cline x1='20' y1='0' x2='0' y2='20' /%3E%3C/svg%3E"); + } + + #menu .close:after { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='20px' height='20px' viewBox='0 0 20 20' zoomAndPan='disable'%3E%3Cstyle%3Eline %7B stroke: %239bf1ff%3B stroke-width: 2%3B %7D%3C/style%3E%3Cline x1='0' y1='0' x2='20' y2='20' /%3E%3Cline x1='20' y1='0' x2='0' y2='20' /%3E%3C/svg%3E"); + opacity: 0; + } + + #menu .close:hover:after, #menu .close:active:after { + opacity: 1; + } + + body.is-ie #menu { + background: rgba(42, 47, 74, 0.975); + } + + body.is-menu-visible #wrapper { + -moz-filter: blur(0.5em); + -webkit-filter: blur(0.5em); + -ms-filter: blur(0.5em); + filter: blur(0.5em); + } + + body.is-menu-visible #menu { + pointer-events: auto; + opacity: 1; + visibility: visible; + } + + body.is-menu-visible #menu .inner { + -moz-transform: none; + -webkit-transform: none; + -ms-transform: none; + transform: none; + opacity: 1; + visibility: visible; + } \ No newline at end of file diff --git a/app/src/main/assets/web/assets/js/dist.js b/app/src/main/assets/web/assets/js/dist.js new file mode 100755 index 000000000..90e42ac81 --- /dev/null +++ b/app/src/main/assets/web/assets/js/dist.js @@ -0,0 +1,8 @@ +/*! + * Powered by uglifiyJS v2.6.1, Build by http://tool.uis.cc/jsmin/ + * build time: Sun Jul 25 2021 19:58:00 GMT+0800 (中国标准时间) +*/ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1c;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Q.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&se(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ce[s])||(o=a.body.appendChild(a.createElement(s)),u=k.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ce[s]=u)))):"none"!==n&&(l[c]="none",Q.set(r,"display",n)));for(c=0;f>c;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;r>n;n++)Q.set(e[n],"globalEval",!t||Q.get(t[n],"globalEval"))}function we(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;h>d;d++)if((o=e[d])||0===o)if("object"===w(o))k.merge(p,o.nodeType?[o]:o);else if(be.test(o)){for(a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+k.htmlPrefilter(o)+u[2],c=u[0];c--;)a=a.lastChild;k.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));for(f.textContent="",d=0;o=p[d++];)if(r&&-1n;n++)k.event.add(t,i,l[i][n]);J.hasData(e)&&(s=J.access(e),u=k.extend({},s),J.set(t,u))}}function Ie(n,r,i,o){r=g.apply([],r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||f>1&&"string"==typeof d&&!y.checkClone&&Le.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),Ie(t,r,i,o)});if(f&&(t=(e=we(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=k.map(ve(e,"script"),Pe)).length;f>c;c++)u=e,c!==p&&(u=k.clone(u,!0,!0),s&&k.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,k.map(a,Re),c=0;s>c;c++)u=a[c],he.test(u.type||"")&&!Q.access(u,"globalEval")&&k.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?k._evalUrl&&!u.noModule&&k._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")}):b(u.textContent.replace(He,""),u,l))}return n}function We(e,t,n){for(var r,i=t?k.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||k.cleanData(ve(r)),r.parentNode&&(n&&oe(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}function _e(e,t,n){var r,i,o,a,s=e.style;return(n=n||Fe(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||oe(e)||(a=k.style(e,t)),!y.pixelBoxStyles()&&$e.test(a)&&Be.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function ze(e,t){return{get:function(){return e()?void delete this.get:(this.get=t).apply(this,arguments)}}}function Ge(e){var t=k.cssProps[e]||Ve[e];return t||(e in Xe?e:Ve[e]=function(e){for(var t=e[0].toUpperCase()+e.slice(1),n=Ue.length;n--;)if((e=Ue[n]+t)in Xe)return e}(e)||e)}function Ze(e,t,n){var r=ne.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function et(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;4>a;a+=2)"margin"===n&&(u+=k.css(e,n+re[a],!0,i)),r?("content"===n&&(u-=k.css(e,"padding"+re[a],!0,i)),"margin"!==n&&(u-=k.css(e,"border"+re[a]+"Width",!0,i))):(u+=k.css(e,"padding"+re[a],!0,i),"padding"!==n?u+=k.css(e,"border"+re[a]+"Width",!0,i):s+=k.css(e,"border"+re[a]+"Width",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function tt(e,t,n){var r=Fe(e),i=(!y.boxSizingReliable()||n)&&"border-box"===k.css(e,"boxSizing",!1,r),o=i,a=_e(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if($e.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||"auto"===a||!parseFloat(a)&&"inline"===k.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===k.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+et(e,t,n||(i?"border":"content"),o,r,a)+"px"}function nt(e,t,n,r,i){return new nt.prototype.init(e,t,n,r,i)}function lt(){it&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(lt):C.setTimeout(lt,k.fx.interval),k.fx.tick())}function ct(){return C.setTimeout(function(){rt=void 0}),rt=Date.now()}function ft(e,t){var n,r=0,i={height:e};for(t=t?1:0;4>r;r+=2-t)i["margin"+(n=re[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function pt(e,t,n){for(var r,i=(dt.tweeners[t]||[]).concat(dt.tweeners["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function dt(o,e,t){var n,a,r=0,i=dt.prefilters.length,s=k.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=rt||ct(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;i>r;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),1>n&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:k.extend({},e),opts:k.extend(!0,{specialEasing:{},easing:k.easing._default},t),originalProperties:e,originalOptions:t,startTime:rt||ct(),duration:t.duration,tweens:[],createTween:function(e,t){var n=k.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;n>t;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for((!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=V(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=k.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing));i>r;r++)if(n=dt.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(k._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return k.map(c,pt,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),k.fx.timer(k.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}function mt(e){return(e.match(R)||[]).join(" ")}function xt(e){return e.getAttribute&&e.getAttribute("class")||""}function bt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(R)||[]}function qt(n,e,r,i){var t;if(Array.isArray(e))k.each(e,function(e,t){r||Nt.test(n)?i(n,t):qt(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)qt(n+"["+t+"]",e[t],r,i)}function Bt(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(R)||[];if(m(t))for(;n=i[r++];)"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function _t(t,i,o,a){function l(e){var r;return s[e]=!0,k.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}var s={},u=t===Wt;return l(i.dataTypes[0])||!s["*"]&&l("*")}function zt(e,t){var n,r,i=k.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&k.extend(!0,e,r),e}var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0},f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;k.fn=k.prototype={jquery:f,constructor:k,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):0>e?this[e+this.length]:this[e]},pushStack:function(e){var t=k.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return k.each(this,e)},map:function(n){return this.pushStack(k.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},k.extend=k.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);u>s;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(k.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||k.isPlainObject(n)?n:{},i=!1,a[t]=k.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},k.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e)||(t=r(e))&&("function"!=typeof(n=v.call(t,"constructor")&&t.constructor)||a.call(n)!==l))},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t){b(e,{nonce:t&&t.nonce})},each:function(e,t){var n,r=0;if(d(e))for(n=e.length;n>r&&!1!==t.call(e[r],r,e[r]);r++);else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},trim:function(e){return null==e?"":(e+"").replace(p,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(d(Object(e))?k.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;o>i;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(d(e))for(r=e.length;r>o;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g.apply([],a)},guid:1,support:y}),"function"==typeof Symbol&&(k.fn[Symbol.iterator]=t[Symbol.iterator]),k.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var h=function(n){function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){for((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;o--;)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){for(var n=e.split("|"),r=n.length;r--;)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){for(var n,r=a([],e.length,o),i=r.length;i--;)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function me(){}function xe(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){for(;e=e[u];)if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[S,p];if(n){for(;e=e[u];)if((1===e.nodeType||f)&&s(e,t,n))return!0}else for(;e=e[u];)if(1===e.nodeType||f)if(i=(o=e[k]||(e[k]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===S&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1s;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[k]&&(v=Ce(v)),y&&!y[k]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;i>r;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v)for(i=Te(p,u),v(i,[],n,r),o=i.length;o--;)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a));if(e){if(y||d){if(y){for(i=[],o=p.length;o--;)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}for(o=p.length;o--;)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1s;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[k]){for(n=++s;r>n&&!b.relative[e[n].type];n++);return Ce(s>1&&we(c),s>1&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(B,"$1"),t,n>s&&Ee(e.slice(s,n)),r>n&&Ee(e=e.slice(n)),r>n&&xe(e))}c.push(t)}return we(c)}var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,k="sizzle"+1*new Date,m=n.document,S=0,r=0,p=ue(),x=ue(),N=ue(),A=ue(),D=function(e,t){return e===t&&(l=!0),0},j={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;r>n;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\.|[\\w-]|[^\x00-\\xa0])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",$=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",F=new RegExp(M+"+","g"),B=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\x00"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];for(i=t.getElementsByName(e),r=0;o=i[r++];)if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){return"undefined"!=typeof t.getElementsByClassName&&E?t.getElementsByClassName(e):void 0},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;a[r]===s[r];)r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1=0}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[k]?a(o):1n?n+t:n]}),even:ve(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=0>n?n+t:n>t?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=0>n?n+t:n;++r0)for(;l--;)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;k.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?k.find.matchesSelector(r,e)?[r]:[]:k.find.matches(e,k.grep(t,function(e){return 1===e.nodeType}))},k.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(k(e).filter(function(){for(t=0;r>t;t++)if(k.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;r>t;t++)k.find(e,i[t],n);return r>1?k.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&N.test(e)?k(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;n>e;e++)if(k.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&k(e);if(!N.test(e))for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1=n&&l--}),this},has:function(e){return e?-1i)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,M,s),l(u,o,I,s)):(u++,t.call(e,l(u,o,M,s),l(u,o,I,s),l(u,o,M,o.notifyWith))):(a!==M&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){k.Deferred.exceptionHook&&k.Deferred.exceptionHook(e,t.stackTrace),i+1>=u&&(a!==I&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(k.Deferred.getStackHook&&(t.stackTrace=k.Deferred.getStackHook()),C.setTimeout(t))}}var u=0;return k.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:M,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:M)),o[2][3].add(l(0,e,m(n)?n:I))}).promise()},promise:function(e){return null!=e?k.extend(e,a):a}},s={};return k.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=k.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1=n&&(W(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();for(;t--;)W(i[t],a(t),o.reject);return o.promise()}});var $=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;k.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&$.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},k.readyException=function(e){C.setTimeout(function(){throw e})};var F=k.Deferred();k.fn.ready=function(e){return F.then(e)["catch"](function(e){k.readyException(e)}),this},k.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--k.readyWait:k.isReady)||(k.isReady=!0)!==e&&0<--k.readyWait||F.resolveWith(E,[k])}}),k.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(k.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var _=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)_(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(k(e),n)})),t))for(;u>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},z=/^-ms-/,U=/-([a-z])/g,G=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};Y.uid=1,Y.prototype={cache:function(e){var t=e[this.expando];return t||(t={},G(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[V(t)]=n;else for(r in t)i[V(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][V(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(V):(t=V(t))in r?[t]:t.match(R)||[]).length;for(;n--;)delete r[t[n]]}(void 0===t||k.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!k.isEmptyObject(t)}};var Q=new Y,J=new Y,K=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;k.extend({hasData:function(e){return J.hasData(e)||Q.hasData(e)},data:function(e,t,n){return J.access(e,t,n)},removeData:function(e,t){J.remove(e,t)},_data:function(e,t,n){return Q.access(e,t,n)},_removeData:function(e,t){Q.remove(e,t)}}),k.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=J.get(o),1===o.nodeType&&!Q.get(o,"hasDataAttrs"))){for(t=a.length;t--;)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=V(r.slice(5)),ee(o,r,i[r]));Q.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){J.set(this,n)}):_(this,function(e){var t;return o&&void 0===e?void 0!==(t=J.get(o,n))?t:void 0!==(t=ee(o,n))?t:void 0:void this.each(function(){J.set(this,n,e)})},null,e,1\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;var me,xe,be=/<|&#?\w+;/;me=E.createDocumentFragment().appendChild(E.createElement("div")),(xe=E.createElement("input")).setAttribute("type","radio"),xe.setAttribute("checked","checked"),xe.setAttribute("name","t"),me.appendChild(xe),y.checkClone=me.cloneNode(!0).cloneNode(!0).lastChild.checked,me.innerHTML="",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v)for(n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;l--;)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){for(l=(t=(t||"").match(R)||[""]).length;l--;)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){for(f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;o--;)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;tn;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;k.extend({htmlPrefilter:function(e){return e.replace(je,"<$1>")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;i>r;r++)s=o[r],u=a[r],"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;i>r;r++)Me(o[r],a[r]);else Me(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=k.event.special,o=0;void 0!==(n=e[o]);o++)if(G(n)){if(t=n[Q.expando]){if(t.events)for(r in t.events)i[r]?k.event.remove(n,r):k.removeEvent(n,r,t.handle);n[Q.expando]=void 0}n[J.expando]&&(n[J.expando]=void 0)}}}),k.fn.extend({detach:function(e){return We(this,e,!0)},remove:function(e){return We(this,e)},text:function(e){return _(this,function(e){return void 0===e?k.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Ie(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Oe(this,e).appendChild(e)})},prepend:function(){return Ie(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Oe(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Ie(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Ie(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(k.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return k.clone(this,e,t)})},html:function(e){return _(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!qe.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=k.htmlPrefilter(e);try{for(;r>n;n++)1===(t=this[n]||{}).nodeType&&(k.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return Ie(this,arguments,function(e){var t=this.parentNode;k.inArray(this,n)<0&&(k.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),k.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){k.fn[e]=function(e){for(var t,n=[],r=k(e),i=r.length-1,o=0;i>=o;o++)t=o===i?this:this.clone(!0),k(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var $e=new RegExp("^("+te+")(?!px)[a-z%]+$","i"),Fe=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},Be=new RegExp(re.join("|"),"i");!function(){function e(){if(u){s.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",u.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",ie.appendChild(s).appendChild(u);var e=C.getComputedStyle(u);n="1%"!==e.top,a=12===t(e.marginLeft),u.style.right="60%",o=36===t(e.right),r=36===t(e.width),u.style.position="absolute",i=12===t(u.offsetWidth/3),ie.removeChild(s),u=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s=E.createElement("div"),u=E.createElement("div");u.style&&(u.style.backgroundClip="content-box",u.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===u.style.backgroundClip,k.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),a},scrollboxSize:function(){return e(),i}}))}();var Ue=["Webkit","Moz","ms"],Xe=E.createElement("div").style,Ve={},Ye=/^(none|table(?!-c[ea]).+)/,Qe=/^--/,Je={position:"absolute",visibility:"hidden",display:"block"},Ke={letterSpacing:"0",fontWeight:"400"};k.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=_e(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=V(t),u=Qe.test(t),l=e.style;if(u||(t=Ge(s)),a=k.cssHooks[t]||k.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ne.exec(n))&&i[1]&&(n=le(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(k.cssNumber[s]?"":"px")),y.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=V(t);return Qe.test(t)||(t=Ge(s)),(a=k.cssHooks[t]||k.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=_e(e,t,r)),"normal"===i&&t in Ke&&(i=Ke[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),k.each(["height","width"],function(e,u){k.cssHooks[u]={get:function(e,t,n){return t?!Ye.test(k.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?tt(e,u,n):ue(e,Je,function(){return tt(e,u,n)}):void 0},set:function(e,t,n){var r,i=Fe(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===k.css(e,"boxSizing",!1,i),s=n?et(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-et(e,u,"border",!1,i)-.5)),s&&(r=ne.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=k.css(e,u)),Ze(0,t,s)}}}),k.cssHooks.marginLeft=ze(y.reliableMarginLeft,function(e,t){return t?(parseFloat(_e(e,"marginLeft"))||e.getBoundingClientRect().left-ue(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px":void 0}),k.each({margin:"",padding:"",border:"Width"},function(i,o){k.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];4>t;t++)n[i+re[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(k.cssHooks[i+o].set=Ze)}),k.fn.extend({css:function(e,t){return _(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Fe(e),i=t.length;i>a;a++)o[t[a]]=k.css(e,t[a],!1,r);return o}return void 0!==n?k.style(e,t,n):k.css(e,t)},e,t,1r;r++)n=e[r],dt.tweeners[n]=dt.tweeners[n]||[],dt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&se(e),v=Q.get(e,"fxshow");for(r in n.queue||(null==(a=k._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,k.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],st.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||k.style(e,r)}if((u=!k.isEmptyObject(t))||!k.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Q.get(e,"display")),"none"===(c=k.css(e,"display"))&&(l?c=l:(fe([e],!0),l=e.style.display||l,c=k.css(e,"display"),fe([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===k.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Q.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&fe([e],!0),p.done(function(){for(r in g||fe([e]),Q.remove(e,"fxshow"),d)k.style(e,r,d[r])})),u=pt(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?dt.prefilters.unshift(e):dt.prefilters.push(e)}}),k.speed=function(e,t,n){var r=e&&"object"==typeof e?k.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return k.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in k.fx.speeds?r.duration=k.fx.speeds[r.duration]:r.duration=k.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&k.dequeue(this,r.queue)},r},k.fn.extend({fadeTo:function(e,t,n,r){return this.filter(se).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=k.isEmptyObject(t),o=k.speed(e,n,r),a=function(){var e=dt(this,k.extend({},t),o);(i||Q.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&!1!==i&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=k.timers,r=Q.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&ut.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||k.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Q.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=k.timers,o=n?n.length:0;for(t.finish=!0, +k.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;o>e;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),k.each(["toggle","show","hide"],function(e,r){var i=k.fn[r];k.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(ft(r,!0),e,t,n)}}),k.each({slideDown:ft("show"),slideUp:ft("hide"),slideToggle:ft("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){k.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),k.timers=[],k.fx.tick=function(){var e,t=0,n=k.timers;for(rt=Date.now();to?u:a?o:0;u>r;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=k(n).val(),a)return t;s.push(t)}return s},set:function(e,t){for(var n,r,i=e.options,o=k.makeArray(t),a=i.length;a--;)((r=i[a]).selected=-11?s:c.bindType||d,(l=(Q.get(o,"events")||{})[e.type]&&Q.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&G(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!G(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),k.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,Ct),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,Ct),k.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=k.extend(new k.Event,n,{type:e,isSimulated:!0});k.event.trigger(r,null,t)}}),k.fn.extend({trigger:function(e,t){return this.each(function(){k.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?k.event.trigger(e,t,n,!0):void 0}}),y.focusin||k.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){k.event.simulate(r,e.target,k.event.fix(e))};k.event.special[r]={setup:function(){var e=this.ownerDocument||this,t=Q.access(e,r);t||e.addEventListener(n,i,!0),Q.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this,t=Q.access(e,r)-1;t?Q.access(e,r,t):(e.removeEventListener(n,i,!0),Q.remove(e,r))}}});var Et=C.location,kt=Date.now(),St=/\?/;k.parseXML=function(e){var t;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&!t.getElementsByTagName("parsererror").length||k.error("Invalid XML: "+e),t};var Nt=/\[\]$/,At=/\r?\n/g,Dt=/^(?:submit|button|image|reset|file)$/i,jt=/^(?:input|select|textarea|keygen)/i;k.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!k.isPlainObject(e))k.each(e,function(){i(this.name,this.value)});else for(n in e)qt(n,e[n],t,i);return r.join("&")},k.fn.extend({serialize:function(){return k.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=k.prop(this,"elements");return e?k.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!k(this).is(":disabled")&&jt.test(this.nodeName)&&!Dt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=k(this).val();return null==n?null:Array.isArray(n)?k.map(n,function(e){return{name:t.name,value:e.replace(At,"\r\n")}}):{name:t.name,value:n.replace(At,"\r\n")}}).get()}});var Lt=/%20/g,Ht=/#.*$/,Ot=/([?&])_=[^&]*/,Pt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Rt=/^(?:GET|HEAD)$/,Mt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Ft=E.createElement("a");Ft.href=Et.href,k.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Et.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Et.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":k.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,k.ajaxSettings),t):zt(k.ajaxSettings,e)},ajaxPrefilter:Bt(It),ajaxTransport:Bt(Wt),ajax:function(e,t){function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(s=function(e,t,n){for(var r,i,o,a,s=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}return o?(o!==u[0]&&u.unshift(o),n[o]):void 0}(v,T,n)),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(k.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(k.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",0>e&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--k.active||k.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=k.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?k(y):k.event,x=k.Deferred(),b=k.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n)for(n={};t=Pt.exec(p);)n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||Et.href)+"").replace(Mt,Et.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(R)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Ft.protocol+"//"+Ft.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=k.param(v.data,v.traditional)),_t(It,v,t,T),h)return T;for(i in(g=k.event&&v.global)&&0==k.active++&&k.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Rt.test(v.type),f=v.url.replace(Ht,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(Lt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(St.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Ot,"$1"),o=(St.test(f)?"&":"?")+"_="+kt++ +o),v.url=f+o),v.ifModified&&(k.lastModified[f]&&T.setRequestHeader("If-Modified-Since",k.lastModified[f]),k.etag[f]&&T.setRequestHeader("If-None-Match",k.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+$t+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=_t(Wt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}:void 0});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");return a||"jsonp"===e.dataTypes[0]?(r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"):void 0}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
    ",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return s>-1&&(r=mt(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{for(t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position");)e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent;e&&"static"===k.css(e,"position");)e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;return x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n?r?r[i]:e[t]:void(r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n)},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){return t?(t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t):void 0})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 01){for(o=0;o1){for(var r=0;r=i&&o>=t};break;case"bottom":h=function(t,e,n,i,o){return n>=i&&o>=n};break;case"middle":h=function(t,e,n,i,o){return e>=i&&o>=e};break;case"top-only":h=function(t,e,n,i,o){return i>=t&&n>=i};break;case"bottom-only":h=function(t,e,n,i,o){return n>=o&&o>=t};break;default:case"default":h=function(t,e,n,i,o){return n>=i&&o>=t}}return c=function(t){var i,o,l,s,r,a,u=this.state,h=!1,c=this.$element.offset();i=n.height(),o=t+i/2,l=t+i,s=this.$element.outerHeight(),r=c.top+e(this.options.top,s,i),a=c.top+s-e(this.options.bottom,s,i),h=this.test(t,o,l,r,a),h!=u&&(this.state=h,h?this.options.enter&&this.options.enter.apply(this.element):this.options.leave&&this.options.leave.apply(this.element)),this.options.scroll&&this.options.scroll.apply(this.element,[(o-r)/(a-r)])},p={id:a,options:u,test:h,handler:c,state:null,element:this,$element:s,timeoutId:null},o[a]=p,s.data("_scrollexId",p.id),p.options.initialize&&p.options.initialize.apply(this),s},jQuery.fn.unscrollex=function(){var e=t(this);if(0==this.length)return e;if(this.length>1){for(var n=0;n0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser});var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e'+$this.text()+"")}),b.join("")},$.fn.panel=function(userConfig){if(0==this.length)return $this;if(this.length>1){for(var i=0;idiffY&&diffY>-1*boundary&&diffX>delta;break;case"right":result=boundary>diffY&&diffY>-1*boundary&&-1*delta>diffX;break;case"top":result=boundary>diffX&&diffX>-1*boundary&&diffY>delta;break;case"bottom":result=boundary>diffX&&diffX>-1*boundary&&-1*delta>diffY}if(result)return $this.touchPosX=null,$this.touchPosY=null,$this._hide(),!1}($this.scrollTop()<0&&0>diffY||ts>th-2&&th+2>ts&&diffY>0)&&(event.preventDefault(),event.stopPropagation())}}),$this.on("click touchend touchstart touchmove",function(event){event.stopPropagation()}),$this.on("click",'a[href="#'+id+'"]',function(event){event.preventDefault(),event.stopPropagation(),config.target.removeClass(config.visibleClass)}),$body.on("click touchend",function(event){$this._hide(event)}),$body.on("click",'a[href="#'+id+'"]',function(event){event.preventDefault(),event.stopPropagation(),config.target.toggleClass(config.visibleClass)}),config.hideOnEscape&&$window.on("keydown",function(event){27==event.keyCode&&$this._hide(event)}),$this},$.fn.placeholder=function(){if("undefined"!=typeof document.createElement("input").placeholder)return $(this);if(0==this.length)return $this;if(this.length>1){for(var i=0;i").append(i.clone()).remove().html().replace(/type="password"/i,'type="text"').replace(/type=password/i,"type=text"));""!=i.attr("id")&&x.attr("id",i.attr("id")+"-polyfill-field"),""!=i.attr("name")&&x.attr("name",i.attr("name")+"-polyfill-field"),x.addClass("polyfill-placeholder").val(x.attr("placeholder")).insertAfter(i),""==i.val()?i.hide():x.hide(),i.on("blur",function(event){event.preventDefault();var x=i.parent().find("input[name="+i.attr("name")+"-polyfill-field]");""==i.val()&&(i.hide(),x.show())}),x.on("focus",function(event){event.preventDefault();var i=x.parent().find("input[name="+x.attr("name").replace("-polyfill-field","")+"]");x.hide(),i.show().focus()}).on("keypress",function(event){event.preventDefault(),x.val("")})}),$this.on("submit",function(){$this.find("input[type=text],input[type=password],textarea").each(function(event){var i=$(this);i.attr("name").match(/-polyfill-field$/)&&i.attr("name",""),i.val()==i.attr("placeholder")&&(i.removeClass("polyfill-placeholder"),i.val(""))})}).on("reset",function(event){event.preventDefault(),$this.find("select").val($("option:first").val()),$this.find("input,textarea").each(function(){var x,i=$(this);switch(i.removeClass("polyfill-placeholder"),this.type){case"submit":case"reset":break;case"password":i.val(i.attr("defaultValue")),x=i.parent().find("input[name="+i.attr("name")+"-polyfill-field]"),""==i.val()?(i.hide(),x.show()):(i.show(),x.hide());break;case"checkbox":case"radio":i.attr("checked",i.attr("defaultValue"));break;case"text":case"textarea":i.val(i.attr("defaultValue")),""==i.val()&&(i.addClass("polyfill-placeholder"),i.val(i.attr("placeholder")));break;default:i.val(i.attr("defaultValue"))}})}),$this},$.prioritize=function($elements,condition){var key="__prioritize";"jQuery"!=typeof $elements&&($elements=$($elements)),$elements.each(function(){var $p,$e=$(this),$parent=$e.parent();if(0!=$parent.length)if($e.data(key)){if(condition)return;$p=$e.data(key),$e.insertAfter($p),$e.removeData(key)}else{if(!condition)return;if($p=$e.prev(),0==$p.length)return;$e.prependTo($parent),$e.data(key,$p)}})}}(jQuery),function($){var $window=$(window),$body=$("body"),$wrapper=$("#wrapper"),$header=$("#header"),$banner=$("#banner");breakpoints({xlarge:["1281px","1680px"],large:["981px","1280px"],medium:["737px","980px"],small:["481px","736px"],xsmall:["361px","480px"],xxsmall:[null,"360px"]}),$.fn._parallax="ie"==browser.name||"edge"==browser.name||browser.mobile?function(){return $(this)}:function(intensity){var $window=$(window),$this=$(this);if(0==this.length||0===intensity)return $this;if(this.length>1){for(var i=0;imedium",on)}),$window.off("load._parallax resize._parallax").on("load._parallax resize._parallax",function(){$window.trigger("scroll")}),$(this)},$window.on("load",function(){window.setTimeout(function(){$body.removeClass("is-preload")},100)}),$window.on("unload pagehide",function(){window.setTimeout(function(){$(".is-transitioning").removeClass("is-transitioning")},250)}),("ie"==browser.name||"edge"==browser.name)&&$body.addClass("is-ie"),$(".scrolly").scrolly({offset:function(){return $header.height()-2}});var $tiles=$(".tiles > article");$tiles.each(function(){var x,$this=$(this),$image=$this.find(".image"),$img=$image.find("img"),$link=$this.find(".link");$this.css("background-image","url("+$img.attr("src")+")"),(x=$img.data("position"))&&$image.css("background-position",x),$image.hide(),$link.length>0&&($x=$link.clone().text("").addClass("primary").appendTo($this),$link=$link.add($x),$link.on("click",function(event){var href=$link.attr("href");event.stopPropagation(),event.preventDefault(),"_blank"==$link.attr("target")?window.open(href):($this.addClass("is-transitioning"),$wrapper.addClass("is-transitioning"),window.setTimeout(function(){location.href=href},500))}))}),$banner.length>0&&$header.hasClass("alt")&&($window.on("resize",function(){$window.trigger("scroll")}),$window.on("load",function(){$banner.scrollex({bottom:$header.height()+10,terminate:function(){$header.removeClass("alt")},enter:function(){$header.addClass("alt")},leave:function(){$header.removeClass("alt"),$header.addClass("reveal")}}),window.setTimeout(function(){$window.triggerHandler("scroll")},100)})),$banner.each(function(){var $this=$(this),$image=$this.find(".image"),$img=$image.find("img");$this._parallax(.275),$image.length>0&&($this.css("background-image","url("+$img.attr("src")+")"),$image.hide())});var $menuInner,$menu=$("#menu");$menu.wrapInner('
    '),$menuInner=$menu.children(".inner"),$menu._locked=!1,$menu._lock=function(){return $menu._locked?!1:($menu._locked=!0,window.setTimeout(function(){$menu._locked=!1},350),!0)},$menu._show=function(){$menu._lock()&&$body.addClass("is-menu-visible")},$menu._hide=function(){$menu._lock()&&$body.removeClass("is-menu-visible")},$menu._toggle=function(){$menu._lock()&&$body.toggleClass("is-menu-visible")},$menuInner.on("click",function(event){event.stopPropagation()}).on("click","a",function(event){var href=$(this).attr("href");event.preventDefault(),event.stopPropagation(),$menu._hide(),window.setTimeout(function(){window.location.href=href},250)}),$menu.appendTo($body).on("click",function(event){event.stopPropagation(),event.preventDefault(),$body.removeClass("is-menu-visible")}).append('Close'),$body.on("click",'a[href="#menu"]',function(event){event.stopPropagation(),event.preventDefault(),$menu._toggle()}).on("click",function(event){$menu._hide()}).on("keydown",function(event){27==event.keyCode&&$menu._hide()})}(jQuery); \ No newline at end of file diff --git a/app/src/main/assets/web/assets/js/md5.js b/app/src/main/assets/web/assets/js/md5.js new file mode 100755 index 000000000..46d2aab7d --- /dev/null +++ b/app/src/main/assets/web/assets/js/md5.js @@ -0,0 +1,256 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} +function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} +function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} +function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } +function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } +function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function md5_vm_test() +{ + return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ +function core_md5(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); + +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) +{ + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); +} +function md5_ff(a, b, c, d, x, s, t) +{ + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function md5_gg(a, b, c, d, x, s, t) +{ + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function md5_hh(a, b, c, d, x, s, t) +{ + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +} +function md5_ii(a, b, c, d, x, s, t) +{ + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Calculate the HMAC-MD5, of a key and some data + */ +function core_hmac_md5(key, data) +{ + var bkey = str2binl(key); + if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ +function str2binl(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + return bin; +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + return str; +} + +/* + * Convert an array of little-endian words to a hex string. + */ +function binl2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of little-endian words to a base-64 string + */ +function binl2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} diff --git a/app/src/main/assets/web/favicon.ico b/app/src/main/assets/web/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..c3d5e6bc00d62252bb516ab1d4ca3d9aa40986aa GIT binary patch literal 4286 zcmb_gdq|X76u*59tx&kK&}1YkB+DZGQ9@uOD$&Et%I9b%Jt(AyuOfPynpyOqd>iF6 zci2p)&0a#ns+$&>>!Vy;N===<869WTsRr_oVb7j(^&3aCb;@oW?{~j@?)m-hd3{5S z&B1@Ju8jV&G6%-wjIm!(@D=k#q4%>Lj*gDZ#l=N%aB#4b%jNd-=g7vH8=ky{@vbgZh!d_ zT3cG6si_IHS}hn11}-|C&fMMIEk1ejM1?#RPEJm8vhgh(TwPu5wr<_(QB(6rWmjjX z>FwKY=zQ}A{^{s|_E)cUVm|O><*o({SaD8$6X2#+5> z_CstMFdO`Lq8@Szd*RP#V;t(A_d|9;5A^owd3|8LnlQ%x`}Xa#_+b6|^>(5tDnEVt z1hdB17$C1y4H<=faIabm={V=&x>wEb&p9+VH<$VN`0$$S>FHrrRaI^uK73Gr4}4K; zfC7~UvXr7l@+&mZH|Bqi-`CeC=H}-9{PgKlwrkfehIJl{*yhpE(OKVL({F%Z9}U2b zyAx81Yc={ss)fP9K{Li%5gi>ZY~H+Cz?@H|yMHZyZA}gK!4`feDc7|F2Dn{ofRgG7 zDZ}+U^oyiJJpp5o78MmGY}&L*Fd9dSz6PIq|Gmmeh>wedg9i@+){mtRYjs0#_xT8v zHB1Pt#evB++0oHax_tR^0egZBbFXq_WSINjWctYSe`v_SdtX;qC$zP;LS0=gl$VzS z_ELzAiGi@NFu?Bvm(b8qh(j*;{e!#r9zxN>Rw%7`3uVuAplUM0!8XxtoNan3%|OfqX%EL3uLO z9^#lbY}nvUn9`s)pq>mxi83h<2p@YjP`*=*l20#Py7U?TjT`w~hkQf&cOZ z{taUfu$JJ;qVyB98AyvT8>P6`SN zEOy8y#ogcE-*GCo!~=7g@?b5>u*92uN;RIEn#${)VnDvVdiAQsHubHHj0}z+8ygE1 z6&3vc*xxNWXwQguUrQJg-gGneCyGNPaUjl=+vHQSMRjoCzyaQGtZh^LDMzTc?cTi` z($mv{YLR%2`_hPWB*t&r-+tCS$T!3RYnuF^Mln-;<7~4?J%Y}T+gOWSK8v|E?$l@W zqky=S50sOi)djwri4UDok`1yo)xIrj@&)+}ZFr$jf1n;UsXnIgpgqzlqBC^%OSxtX zKIzbT?A6pC$RAEqvdg`uvxkfySr{K;2vb#^7JZlFf|yj56k8WbDUY#(p5FV{EP@DfnLsQw8_tNc%MR Uj-MkNWQ%N)ZNlJKW*?yOA4v{VRR910 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/help/css/github-markdown-light.min.css b/app/src/main/assets/web/help/css/github-markdown-light.min.css new file mode 100755 index 000000000..10eff1eff --- /dev/null +++ b/app/src/main/assets/web/help/css/github-markdown-light.min.css @@ -0,0 +1 @@ +.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;margin:0;color:#24292f;background-color:#fff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .octicon{display:inline-block;fill:currentColor;vertical-align:text-bottom}.markdown-body h1:hover .anchor .octicon-link:before,.markdown-body h2:hover .anchor .octicon-link:before,.markdown-body h3:hover .anchor .octicon-link:before,.markdown-body h4:hover .anchor .octicon-link:before,.markdown-body h5:hover .anchor .octicon-link:before,.markdown-body h6:hover .anchor .octicon-link:before{width:16px;height:16px;content:' ';display:inline-block;background-color:currentColor;-webkit-mask-image:url("data:image/svg+xml,");mask-image:url("data:image/svg+xml,")}.markdown-body details,.markdown-body figcaption,.markdown-body figure{display:block}.markdown-body summary{display:list-item}.markdown-body [hidden]{display:none!important}.markdown-body a{background-color:transparent;color:#0969da;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body abbr[title]{border-bottom:none;text-decoration:underline dotted}.markdown-body b,.markdown-body strong{font-weight:600}.markdown-body dfn{font-style:italic}.markdown-body h1{margin:.67em 0;font-weight:600;padding-bottom:.3em;font-size:2em;border-bottom:1px solid #d7dde3}.markdown-body mark{background-color:#fff8c5;color:#24292f}.markdown-body small{font-size:90%}.markdown-body sub,.markdown-body sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.markdown-body sub{bottom:-.25em}.markdown-body sup{top:-.5em}.markdown-body img{border-style:none;max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code,.markdown-body kbd,.markdown-body pre,.markdown-body samp{font-family:monospace,monospace;font-size:1em}.markdown-body figure{margin:1em 40px}.markdown-body hr{box-sizing:content-box;overflow:hidden;background:0 0;border-bottom:1px solid #d7dde3;height:.25em;padding:0;margin:24px 0;background-color:#d0d7de;border:0}.markdown-body input{font:inherit;margin:0;overflow:visible;font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body [type=button],.markdown-body [type=reset],.markdown-body [type=submit]{-webkit-appearance:button}.markdown-body [type=button]::-moz-focus-inner,.markdown-body [type=reset]::-moz-focus-inner,.markdown-body [type=submit]::-moz-focus-inner{border-style:none;padding:0}.markdown-body [type=button]:-moz-focusring,.markdown-body [type=reset]:-moz-focusring,.markdown-body [type=submit]:-moz-focusring{outline:1px dotted ButtonText}.markdown-body [type=checkbox],.markdown-body [type=radio]{box-sizing:border-box;padding:0}.markdown-body [type=number]::-webkit-inner-spin-button,.markdown-body [type=number]::-webkit-outer-spin-button{height:auto}.markdown-body [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.markdown-body [type=search]::-webkit-search-cancel-button,.markdown-body [type=search]::-webkit-search-decoration{-webkit-appearance:none}.markdown-body ::-webkit-input-placeholder{color:inherit;opacity:.54}.markdown-body ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.markdown-body a:hover{text-decoration:underline}.markdown-body hr::before{display:table;content:""}.markdown-body hr::after{display:table;clear:both;content:""}.markdown-body table{border-spacing:0;border-collapse:collapse;display:block;width:max-content;max-width:100%;overflow:auto}.markdown-body td,.markdown-body th{padding:0}.markdown-body details summary{cursor:pointer}.markdown-body details:not([open])>:not(summary){display:none!important}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;line-height:10px;color:#24292f;vertical-align:middle;background-color:#f6f8fa;border:solid 1px rgba(175,184,193,.2);border-bottom-color:rgba(175,184,193,.2);border-radius:6px;box-shadow:inset 0 -1px 0 rgba(175,184,193,.2)}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h2{font-weight:600;padding-bottom:.3em;font-size:1.5em;border-bottom:1px solid #d7dde3}.markdown-body h3{font-weight:600;font-size:1.25em}.markdown-body h4{font-weight:600;font-size:1em}.markdown-body h5{font-weight:600;font-size:.875em}.markdown-body h6{font-weight:600;font-size:.85em;color:#57606a}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0;padding:0 1em;color:#57606a;border-left:.25em solid #d0d7de}.markdown-body ol,.markdown-body ul{margin-top:0;margin-bottom:0;padding-left:2em}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code,.markdown-body tt{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;font-size:12px;word-wrap:normal}.markdown-body .octicon{display:inline-block;overflow:visible!important;vertical-align:text-bottom;fill:currentColor}.markdown-body ::placeholder{color:#6e7781;opacity:1}.markdown-body input::-webkit-inner-spin-button,.markdown-body input::-webkit-outer-spin-button{margin:0;-webkit-appearance:none;appearance:none}.markdown-body .pl-c{color:#6e7781}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0550ae}.markdown-body .pl-e,.markdown-body .pl-en{color:#8250df}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#24292f}.markdown-body .pl-ent{color:#116329}.markdown-body .pl-k{color:#cf222e}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#0a3069}.markdown-body .pl-smw,.markdown-body .pl-v{color:#953800}.markdown-body .pl-bu{color:#82071e}.markdown-body .pl-ii{color:#f6f8fa;background-color:#82071e}.markdown-body .pl-c2{color:#f6f8fa;background-color:#cf222e}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#116329}.markdown-body .pl-ml{color:#3b2300}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#0550ae}.markdown-body .pl-mi{font-style:italic;color:#24292f}.markdown-body .pl-mb{font-weight:700;color:#24292f}.markdown-body .pl-md{color:#82071e;background-color:#ffebe9}.markdown-body .pl-mi1{color:#116329;background-color:#dafbe1}.markdown-body .pl-mc{color:#953800;background-color:#ffd8b5}.markdown-body .pl-mi2{color:#eaeef2;background-color:#0550ae}.markdown-body .pl-mdr{font-weight:700;color:#8250df}.markdown-body .pl-ba{color:#57606a}.markdown-body .pl-sg{color:#8c959f}.markdown-body .pl-corl{text-decoration:underline;color:#0a3069}.markdown-body [data-catalyst]{display:block}.markdown-body g-emoji{font-family:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1em;font-style:normal!important;font-weight:400;line-height:1;vertical-align:-.075em}.markdown-body g-emoji img{width:1em;height:1em}.markdown-body::before{display:table;content:""}.markdown-body::after{display:table;clear:both;content:""}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .absent{color:#cf222e}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:0}.markdown-body blockquote,.markdown-body details,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body sup>a::before{content:"["}.markdown-body sup>a::after{content:"]"}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#24292f;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1 code,.markdown-body h1 tt,.markdown-body h2 code,.markdown-body h2 tt,.markdown-body h3 code,.markdown-body h3 tt,.markdown-body h4 code,.markdown-body h4 tt,.markdown-body h5 code,.markdown-body h5 tt,.markdown-body h6 code,.markdown-body h6 tt{padding:0 .2em;font-size:inherit}.markdown-body ol.no-list,.markdown-body ul.no-list{padding:0;list-style-type:none}.markdown-body ol[type="1"]{list-style-type:decimal}.markdown-body ol[type=a]{list-style-type:lower-alpha}.markdown-body ol[type=i]{list-style-type:lower-roman}.markdown-body div>ol:not([type]){list-style-type:decimal}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table th{font-weight:600}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #d0d7de}.markdown-body table tr{background-color:#fff;border-top:1px solid #d7dde3}.markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.markdown-body table img{background-color:transparent}.markdown-body img[align=right]{padding-left:20px}.markdown-body img[align=left]{padding-right:20px}.markdown-body .emoji{max-width:none;vertical-align:text-top;background-color:transparent}.markdown-body span.frame{display:block;overflow:hidden}.markdown-body span.frame>span{display:block;float:left;width:auto;padding:7px;margin:13px 0 0;overflow:hidden;border:1px solid #d0d7de}.markdown-body span.frame span img{display:block;float:left}.markdown-body span.frame span span{display:block;padding:5px 0 0;clear:both;color:#24292f}.markdown-body span.align-center{display:block;overflow:hidden;clear:both}.markdown-body span.align-center>span{display:block;margin:13px auto 0;overflow:hidden;text-align:center}.markdown-body span.align-center span img{margin:0 auto;text-align:center}.markdown-body span.align-right{display:block;overflow:hidden;clear:both}.markdown-body span.align-right>span{display:block;margin:13px 0 0;overflow:hidden;text-align:right}.markdown-body span.align-right span img{margin:0;text-align:right}.markdown-body span.float-left{display:block;float:left;margin-right:13px;overflow:hidden}.markdown-body span.float-left span{margin:13px 0 0}.markdown-body span.float-right{display:block;float:right;margin-left:13px;overflow:hidden}.markdown-body span.float-right>span{display:block;margin:13px auto 0;overflow:hidden;text-align:right}.markdown-body code,.markdown-body tt{padding:.2em .4em;margin:0;font-size:85%;background-color:rgba(175,184,193,.2);border-radius:6px}.markdown-body code br,.markdown-body tt br{display:none}.markdown-body del code{text-decoration:inherit}.markdown-body pre code{font-size:100%}.markdown-body pre>code{padding:0;margin:0;word-break:normal;white-space:pre;background:0 0;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:6px}.markdown-body pre code,.markdown-body pre tt{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body .csv-data td,.markdown-body .csv-data th{padding:5px;overflow:hidden;font-size:12px;line-height:1;text-align:left;white-space:nowrap}.markdown-body .csv-data .blob-num{padding:10px 8px 9px;text-align:right;background:#fff;border:0}.markdown-body .csv-data tr{border-top:0}.markdown-body .csv-data th{font-weight:600;background:#f6f8fa;border-top:0}.markdown-body .footnotes{font-size:12px;color:#57606a;border-top:1px solid #d0d7de}.markdown-body .footnotes ol{padding-left:16px}.markdown-body .footnotes li{position:relative}.markdown-body .footnotes li:target::before{position:absolute;top:-8px;right:-8px;bottom:-8px;left:-24px;pointer-events:none;content:"";border:2px solid #0969da;border-radius:6px}.markdown-body .footnotes li:target{color:#24292f}.markdown-body .footnotes .data-footnote-backref g-emoji{font-family:monospace}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item label{font-weight:400}.markdown-body .task-list-item.enabled label{cursor:pointer}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item .handle{display:none}.markdown-body .task-list-item-checkbox{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox{margin:0 -1.6em .25em .2em}.markdown-body ::-webkit-calendar-picker-indicator{filter:invert(50%)} \ No newline at end of file diff --git a/app/src/main/assets/web/help/css/highlight.min.css b/app/src/main/assets/web/help/css/highlight.min.css new file mode 100755 index 000000000..033028600 --- /dev/null +++ b/app/src/main/assets/web/help/css/highlight.min.css @@ -0,0 +1,132 @@ +.theme-vs2015-min pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; +} + +.theme-vs2015-min code.hljs { + padding: 3px 5px; +} + +.theme-vs2015-min .hljs { + background: #1e1e1e; + color: #dcdcdc; + padding: 1rem; + display: block; +} + +.theme-vs2015-min .hljs-keyword, +.theme-vs2015-min .hljs-literal, +.theme-vs2015-min .hljs-name, +.theme-vs2015-min .hljs-symbol { + color: #569cd6; +} + +.theme-vs2015-min .hljs-link { + color: #569cd6; + text-decoration: underline; +} + +.theme-vs2015-min .hljs-built_in, +.theme-vs2015-min .hljs-type { + color: #4ec9b0; +} + +.theme-vs2015-min .hljs-class, +.theme-vs2015-min .hljs-number { + color: #b8d7a3; +} + +.theme-vs2015-min .hljs-meta .hljs-string, +.theme-vs2015-min .hljs-string { + color: #d69d85; +} + +.theme-vs2015-min .hljs-regexp, +.theme-vs2015-min .hljs-template-tag { + color: #9a5334; +} + +.theme-vs2015-min .hljs-formula, +.theme-vs2015-min .hljs-function, +.theme-vs2015-min .hljs-params, +.theme-vs2015-min .hljs-subst, +.theme-vs2015-min .hljs-title { + color: #dcdcdc; +} + +.theme-vs2015-min .hljs-comment, +.theme-vs2015-min .hljs-quote { + color: #57a64a; + font-style: italic; +} + +.theme-vs2015-min .hljs-doctag { + color: #608b4e; +} + +.theme-vs2015-min .hljs-meta, +.theme-vs2015-min .hljs-meta .hljs-keyword, +.theme-vs2015-min .hljs-tag { + color: #9b9b9b; +} + +.theme-vs2015-min .hljs-template-variable, +.theme-vs2015-min .hljs-variable { + color: #bd63c5; +} + +.theme-vs2015-min .hljs-attr, +.theme-vs2015-min .hljs-attribute { + color: #9cdcfe; +} + +.theme-vs2015-min .hljs-section { + color: gold; +} + +.theme-vs2015-min .hljs-emphasis { + font-style: italic; +} + +.theme-vs2015-min .hljs-strong { + font-weight: 700; +} + +.theme-vs2015-min .hljs-bullet, +.theme-vs2015-min .hljs-selector-attr, +.theme-vs2015-min .hljs-selector-class, +.theme-vs2015-min .hljs-selector-id, +.theme-vs2015-min .hljs-selector-pseudo, +.theme-vs2015-min .hljs-selector-tag { + color: #d7ba7d; +} + +.theme-vs2015-min .hljs-addition { + background-color: #144212; + display: inline-block; + width: 100%; +} + +.theme-vs2015-min .hljs-deletion { + background-color: #600; + display: inline-block; + width: 100%; +} + +pre { + position: relative; +} + +small { + position: absolute; + top: 0; + right: 0; + text-transform: uppercase; + font-weight: 700; + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + background-color: #7777; + border-bottom-left-radius: 0.375rem; + color: #dcdcdc; +} diff --git a/app/src/main/assets/web/help/index.html b/app/src/main/assets/web/help/index.html new file mode 100755 index 000000000..a24023959 --- /dev/null +++ b/app/src/main/assets/web/help/index.html @@ -0,0 +1,21 @@ + + + + + help + + + + + + +
    + + diff --git a/app/src/main/assets/web/help/js/highlight.min.js b/app/src/main/assets/web/help/js/highlight.min.js new file mode 100755 index 000000000..f43ba9aa9 --- /dev/null +++ b/app/src/main/assets/web/help/js/highlight.min.js @@ -0,0 +1,1213 @@ +/*! + Highlight.js v11.9.0 (git: f47103d4f1) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ + var hljs=function(){"use strict";function e(n){ + return n instanceof Map?n.clear=n.delete=n.set=()=>{ + throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ + throw Error("set is read-only") + }),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ + const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) + })),n}class n{constructor(e){ + void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} + ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ + return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") + }function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] + ;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope + ;class r{constructor(e,n){ + this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ + this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ + if(e.startsWith("language:"))return e.replace("language:","language-") + ;if(e.includes(".")){const t=e.split(".") + ;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") + }return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} + closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ + this.buffer+=``}}const s=(e={})=>{const n={children:[]} + ;return Object.assign(n,e),n};class o{constructor(){ + this.rootNode=s(),this.stack=[this.rootNode]}get top(){ + return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ + this.top.children.push(e)}openNode(e){const n=s({scope:e}) + ;this.add(n),this.stack.push(n)}closeNode(){ + if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ + for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} + walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ + return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), + n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ + "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ + o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} + addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ + this.closeNode()}__addSublanguage(e,n){const t=e.root + ;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ + return new r(this,this.options).value()}finalize(){ + return this.closeAllNodes(),!0}}function c(e){ + return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} + function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} + function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ + const n=e[e.length-1] + ;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} + })(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} + function p(e){return RegExp(e.toString()+"|").exec("").length-1} + const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ + ;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t + ;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} + i+=a.substring(0,e.index), + a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], + "("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} + const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ + begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", + illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", + contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, + contains:[]},t);i.contains.push({scope:"doctag", + begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", + end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) + ;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) + ;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i + },M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ + __proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ + scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, + C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", + begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ + "on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ + n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, + MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, + NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, + PHRASAL_WORDS_MODE:{ + begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ + },QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, + end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, + RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", + SHEBANG:(e={})=>{const n=/^#![ ]*\// + ;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, + end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, + TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, + UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ + "."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ + void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ + n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", + e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, + void 0===e.relevance&&(e.relevance=0))}function I(e,n){ + Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ + if(e.match){ + if(e.begin||e.end)throw Error("begin & end are not supported with match") + ;e.begin=e.match,delete e.match}}function B(e,n){ + void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return + ;if(e.starts)throw Error("beforeMatch cannot be used with starts") + ;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] + })),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ + relevance:0,contains:[Object.assign(t,{endsParent:!0})] + },e.relevance=0,delete t.beforeMatch + },z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" + ;function U(e,n,t=F){const a=Object.create(null) + ;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ + Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ + n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") + ;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ + return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ + console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ + P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) + },G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} + ;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) + ;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ + e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, + delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ + _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope + }),(e=>{if(Array.isArray(e.begin)){ + if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), + G + ;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), + G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ + if(Array.isArray(e.end)){ + if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), + G + ;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), + G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ + function n(n,t){ + return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) + }class t{constructor(){ + this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} + addRule(e,n){ + n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), + this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) + ;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" + }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex + ;const n=this.matcherRe.exec(e);if(!n)return null + ;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] + ;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ + this.rules=[],this.multiRegexes=[], + this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ + if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t + ;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), + n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ + return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ + this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ + const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex + ;let t=n.exec(e) + ;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ + const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} + return t&&(this.regexIndex+=t.position+1, + this.regexIndex===this.count&&this.considerAll()),t}} + if(e.compilerExtensions||(e.compilerExtensions=[]), + e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") + ;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r + ;if(r.isCompiled)return o + ;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), + r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null + ;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), + l=r.keywords.$pattern, + delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), + o.keywordPatternRe=n(l,!0), + s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), + r.end&&(o.endRe=n(o.end)), + o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), + r.illegal&&(o.illegalRe=n(r.illegal)), + r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ + variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ + starts:e.starts?a(e.starts):null + }):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) + })),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i + ;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" + }))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" + }),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ + return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ + constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} + const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ + const a=Object.create(null),i=Object.create(null),r=[];let s=!0 + ;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ + disableAutodetect:!0,name:"Plain text",contains:[]};let p={ + ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, + languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", + cssSelector:"pre code",languages:null,__emitter:l};function _(e){ + return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" + ;"object"==typeof n?(a=e, + t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), + q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), + i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) + ;const s=r.result?r.result:f(r.language,r.code,t) + ;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ + const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) + ;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" + ;for(;n;){t+=A.substring(e,n.index) + ;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ + const[e,a]=r + ;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ + const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] + ;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a + ;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ + if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ + if(!a[x.subLanguage])return void S.addText(A) + ;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top + }else e=E(A,x.subLanguage.length?x.subLanguage:null) + ;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) + })():c(),A=""}function g(e,n){ + ""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 + ;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} + const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} + function b(e,n){ + return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), + e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), + A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ + value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) + ;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) + ;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ + for(;e.endsParent&&e.parent;)e=e.parent;return e}} + if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ + return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ + const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x + ;x.endScope&&x.endScope._wrap?(d(), + g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), + u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), + d(),r.excludeEnd&&(A=n));do{ + x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent + }while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} + let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 + ;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ + if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) + ;throw n.languageName=e,n.badRule=y.rule,n}return 1} + if(y=r,"begin"===r.type)return(e=>{ + const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] + ;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) + ;return a.skip?A+=t:(a.excludeBegin&&(A+=t), + d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) + ;if("illegal"===r.type&&!i){ + const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') + ;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} + if("illegal"===r.type&&""===o)return 1 + ;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") + ;return A+=o,o.length}const w=v(e) + ;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') + ;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] + ;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) + ;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ + if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ + R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T + ;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) + ;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, + value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ + if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), + illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, + context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ + language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} + ;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ + const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} + ;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) + ;i.unshift(t);const r=i.sort(((e,n)=>{ + if(e.relevance!==n.relevance)return n.relevance-e.relevance + ;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 + ;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s + ;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ + let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" + ;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) + ;return n||(H(o.replace("{}",t[1])), + H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} + return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return + ;if(x("before:highlightElement",{el:e,language:t + }),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) + ;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), + console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), + console.warn("The element with unescaped HTML:"), + console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) + ;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) + ;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t + ;e.classList.add("hljs"),e.classList.add("language-"+a) + })(e,t,r.language),e.result={language:r.language,re:r.relevance, + relevance:r.relevance},r.secondBest&&(e.secondBest={ + language:r.secondBest.language,relevance:r.secondBest.relevance + }),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ + "loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 + }function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} + function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ + i[e.toLowerCase()]=n}))}function k(e){const n=v(e) + ;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ + e[t]&&e[t](n)}))} + "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ + N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, + highlightElement:y, + highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), + q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, + initHighlighting:()=>{ + w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, + initHighlightingOnLoad:()=>{ + w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") + },registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ + if(K("Language definition for '{}' could not be registered.".replace("{}",e)), + !s)throw n;K(n),i=c} + i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ + languageName:e})},unregisterLanguage:e=>{delete a[e] + ;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, + listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, + autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ + e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ + e["before:highlightBlock"](Object.assign({block:n.el},n)) + }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ + e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, + removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ + s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, + lookahead:d,either:m,optional:u,anyNumberOfTimes:g} + ;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t + },te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ + scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ + scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, + FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, + ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", + contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ + scope:"number", + begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", + relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} + }),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) + ;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ + className:"number",variants:[{ + begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ + begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ + begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ + begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ + begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ + begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], + relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} + const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) + ;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, + end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ + const t=e[0].length+e.index,a=e.input[t] + ;if("<"===a||","===a)return void n.ignoreMatch();let i + ;">"===a&&(((e,{after:n})=>{const t="",M={ + match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], + keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} + ;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ + PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, + contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ + label:"use_strict",className:"meta",relevance:10, + begin:/^\s*['"]use (strict|asm)['"]/ + },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ + className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ + begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", + keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ + className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ + className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ + className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, + excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, + relevance:0},{variants:[{begin:"<>",end:""},{ + match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, + "on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ + begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ + beginKeywords:"while if switch catch for"},{ + begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", + returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, + className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, + relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, + contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, + className:"variable.constant"},E,k,{match:/\$[(.]/}]}} + const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] + ;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ + begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} + ;Object.assign(t,{className:"variable",variants:[{ + begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ + className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ + begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, + end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, + contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, + end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] + },l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 + }),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, + contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ + name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, + keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], + literal:["true","false"], + built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] + },contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ + match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, + grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] + }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ + className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ + match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ + begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ + begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ + begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ + className:"number",variants:[{begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + },{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ + keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ + className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ + className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 + },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ + keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], + type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], + literal:"true false NULL", + built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" + },b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ + begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], + keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, + contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ + begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, + keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ + begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], + relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, + keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, + end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] + }]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, + disableAutodetect:!0,illegal:"=]/,contains:[{ + beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, + strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ + contains:[{begin:/\\\n/}] + }),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ + className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ + begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ + begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", + end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ + begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ + className:"number",variants:[{begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" + },{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ + keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" + },contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ + className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ + className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 + },g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ + type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], + keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], + literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], + _type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] + },b={className:"function.dispatch",relevance:0,keywords:{ + _hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] + }, + begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) + },m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ + begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], + keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, + contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", + begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, + keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ + begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ + begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ + className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, + contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, + relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] + },s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", + aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ + match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], + className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ + keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), + built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], + literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ + begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ + begin:"\\b(0b[01']+)"},{ + begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ + begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" + }],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] + },r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, + keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, + end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ + },e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ + begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, + contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) + ;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], + o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ + illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] + },u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] + },b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ + begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], + keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, + contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ + begin:"\x3c!--|--\x3e"},{begin:""}]}] + }),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", + end:"$",keywords:{ + keyword:"if else elif endif define undef warning error line region endregion pragma checksum" + }},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, + illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" + },t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", + relevance:0,end:/[{;=]/,illegal:/[^\s:]/, + contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ + beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, + contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", + begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ + className:"string",begin:/"/,end:/"/}]},{ + beginKeywords:"new return throw await else",relevance:0},{className:"function", + begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, + end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ + beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", + relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, + contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", + begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, + contains:[g,a,e.C_BLOCK_COMMENT_MODE] + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ + const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ + name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ + keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, + contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ + },t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 + },{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 + },t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ + begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] + },t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ + begin:/:/,end:/[;}{]/, + contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ + begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" + },contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, + excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", + relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ + },{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ + $pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ + begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ + className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ + const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ + className:"meta",relevance:10, + match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) + },{className:"comment",variants:[{ + begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), + end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ + className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, + end:/$/}]}},grmr_go:e=>{const n={ + keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], + type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], + literal:["true","false","iota","nil"], + built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] + };return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], + case_insensitive:!0,disableAutodetect:!1,keywords:{ + keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], + literal:["true","false","null"]}, + contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ + scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", + begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, + end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ + scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), + relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ + className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ + begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, + end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ + begin:/\$\{(.*?)\}/}]},r={className:"literal", + begin:/\bon|off|true|false|yes|no\b/},s={className:"string", + contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ + begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] + },o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 + },l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ + name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, + contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ + begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), + className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ + const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ + keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], + literal:["false","true","null"], + type:["char","boolean","long","float","int","byte","short","double"], + built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ + begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, + end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} + ;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, + contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, + relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ + begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, + className:"string",contains:[e.BACKSLASH_ESCAPE] + },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ + match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ + 1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ + begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", + 3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", + 3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ + beginKeywords:"new throw return else",relevance:0},{ + begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ + 2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, + end:/\)/,keywords:i,relevance:0, + contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, + grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", + beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ + className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ + match:/[{}[\],:]/,className:"punctuation",relevance:0 + },e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], + illegal:"\\S"}},grmr_kotlin:e=>{const n={ + keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", + built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", + literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" + },a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ + className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", + variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", + illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, + contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ + className:"meta", + begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" + },o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, + end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] + },l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ + variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, + contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], + {name:"Kotlin",aliases:["kt","kts"],keywords:n, + contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", + begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", + begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", + begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", + returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ + begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, + contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, + keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, + endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, + endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 + },e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ + begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ + 3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, + illegal:"extends implements",contains:[{ + beginKeywords:"public protected internal private constructor" + },e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, + excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, + excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", + end:"$",illegal:"\n"},l]}},grmr_less:e=>{ + const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ + className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, + relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", + attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, + relevance:0} + ;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ + begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", + excludeEnd:!0} + },n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ + className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 + },n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ + begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, + contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", + returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ + },n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", + end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] + },m={className:"keyword", + begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", + starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ + className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a + }],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ + begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, + returnEnd:!0,illegal:"[<='$\"]",relevance:0, + contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ + begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" + },n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ + className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ + className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, + end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ + begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} + ;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), + {name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, + grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] + },i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 + })];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, + literal:"true false nil", + keyword:"and break do else elseif end for goto if in local not or repeat return then until while", + built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" + },contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", + contains:[e.inherit(e.TITLE_MODE,{ + begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", + begin:"\\(",endsWithParent:!0,contains:i}].concat(i) + },e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", + begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ + className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", + contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ + const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ + variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ + begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, + relevance:2},{ + begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), + relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ + begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ + },{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, + returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", + excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", + end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], + variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] + },i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ + begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] + }),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) + ;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) + })),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ + className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ + begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", + contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", + end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, + end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ + begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ + begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", + contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ + begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ + className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ + className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ + const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, + keyword:["@interface","@class","@protocol","@implementation"]};return{ + name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], + keywords:{"variable.language":["this","super"],$pattern:n, + keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], + literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], + built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], + type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] + },illegal:"/,end:/$/,illegal:"\\n" + },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", + begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, + contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, + relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ + $pattern:/[\w.]+/, + keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" + },i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, + end:/\}/},s={variants:[{begin:/\$\d/},{ + begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") + },{begin:/[$%@][^\s\w{]/,relevance:0}] + },o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ + const r="\\1"===i?i:n.concat(i,a) + ;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) + },d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ + endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ + begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", + end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ + begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", + relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", + contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", + contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ + begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", + begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", + relevance:0},{ + begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", + keywords:"split return print reverse grep",relevance:0, + contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ + begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ + begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ + className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ + begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 + }),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ + begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", + end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ + begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", + subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] + }];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, + contains:g}},grmr_php:e=>{ + const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ + scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ + begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null + }),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ + illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ + begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, + contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ + n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ + n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ + begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ + begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ + begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ + begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" + }],relevance:0 + },g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ + keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ + n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) + })),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ + match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ + 1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ + match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" + }},{match:[/::/,/class/],scope:{2:"variable.language"}},{ + match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", + 3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], + scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", + 3:"variable.language"}}]},E={scope:"attr", + match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, + begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] + },N={relevance:0, + match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], + scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) + ;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, + keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, + endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ + begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, + contains:["self",...w]},...w,{scope:"meta",match:i}] + },e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ + scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, + keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, + contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ + begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ + begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ + match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ + scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, + excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" + },e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", + begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, + contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ + beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", + illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ + beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ + beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, + contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ + beginKeywords:"use",relevance:0,end:";",contains:[{ + match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} + },grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ + begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", + end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 + },e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, + skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, + contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", + aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ + const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ + $pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, + built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], + literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], + type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] + },r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, + end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ + className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ + begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, + contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ + begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, + contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ + begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, + contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, + end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, + relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ + begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, + end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, + contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, + contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] + },c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ + className:"number",relevance:0,variants:[{ + begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ + begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ + begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` + },{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` + }]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, + contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ + className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, + end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, + contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ + name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, + illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", + relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ + 1:"keyword",3:"title.function"},contains:[m]},{variants:[{ + match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], + scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ + className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, + grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", + starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ + begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ + const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) + ;return{name:"R",keywords:{$pattern:t, + keyword:"function if in break next repeat else for while", + literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", + built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" + },contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, + starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), + endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ + scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 + }]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] + }),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], + variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ + }),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', + relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ + 1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, + match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ + 2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, + match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ + match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", + contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ + const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ + "variable.constant":["__FILE__","__LINE__","__ENCODING__"], + "variable.language":["self","super"], + keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], + built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], + literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ + begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] + }),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 + }),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, + end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], + variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ + begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ + begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, + end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ + begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ + begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ + begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ + begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ + begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), + contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, + contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", + relevance:0,variants:[{ + begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ + begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" + },{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ + begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ + begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ + className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, + keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ + match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", + 4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ + 2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ + 1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, + className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ + match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ + begin:e.IDENT_RE+"::"},{className:"symbol", + begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", + begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", + begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ + className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, + relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", + keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], + illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ + begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", + end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) + ;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} + },{className:"meta.prompt", + begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", + starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", + aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, + contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, + grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, + begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) + },a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] + ;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, + keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], + literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, + grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", + begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", + case_insensitive:!0,illegal:"[=/|']", + contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ + className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ + className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 + },n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", + begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", + begin:":("+a.join("|")+")"},{className:"selector-pseudo", + begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, + contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", + begin:"\\b("+ce.join("|")+")\\b"},{ + begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" + },{begin:/:/,end:/[;}{]/,relevance:0, + contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] + },{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ + begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, + keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, + className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" + },r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] + },n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", + aliases:["console","shellsession"],contains:[{className:"meta.prompt", + begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, + subLanguage:"bash"}}]}),grmr_sql:e=>{ + const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ + begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} + ;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ + $pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t + ;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) + })(l,{when:e=>e.length<3}),literal:a,type:i, + built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] + },contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, + keyword:l.concat(s),literal:a,type:i}},{className:"type", + begin:n.either("double precision","large object","with timezone","without timezone") + },c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", + variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, + contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ + className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, + relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 + },t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ + match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), + relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ + className:"keyword", + match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ + $pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ + match:b(/\./,m(...De)),relevance:0},{className:"built_in", + match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ + className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] + }],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, + variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ + match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ + },{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ + match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] + }),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) + }),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ + }),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] + }),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ + className:"string", + variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] + },k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, + contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, + contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, + contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ + scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) + },C=[A,{className:"variable",match:/\$\d+/},{className:"variable", + match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ + contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ + scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ + match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", + match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") + },{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ + match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ + begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) + ;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ + match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 + },...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, + keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, + contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, + relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", + match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ + match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", + 3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ + match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, + contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ + 1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ + 1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} + ;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) + ;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, + end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, + contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", + end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ + className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] + },F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 + },S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ + const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ + beginKeywords:"namespace",end:/\{/,excludeEnd:!0, + contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, + excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, + contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, + keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), + literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", + begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) + ;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} + ;return Object.assign(n.keywords,s), + n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), + l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, + begin:/^\s*['"]use strict['"]/ + }),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ + name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ + const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ + className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ + begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ + begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] + },o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] + }),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) + ;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, + classNameAliases:{label:"symbol"},keywords:{ + keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", + built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", + type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", + literal:"true false nothing"}, + illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ + className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, + end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, + variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ + },{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ + begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ + className:"label",begin:/^\w+:/},o,l,{className:"meta", + begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, + end:/$/,keywords:{ + keyword:"const disable else elseif enable end externalsource if region then"}, + contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) + ;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, + keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] + },contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], + className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ + match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ + begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", + 3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, + className:"type"},{className:"keyword", + match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ + },{className:"number",relevance:0, + match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ + }]}},grmr_xml:e=>{ + const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ + className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, + contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] + },r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ + className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ + endsWithParent:!0,illegal:/`]+/}]}]}]};return{ + name:"HTML, XML", + aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], + case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ + className:"meta",begin://,contains:[i,r,o,s]}]}] + },e.COMMENT(//,{relevance:10}),{begin://, + relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, + relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", + begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ + end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", + begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ + end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ + className:"tag",begin:/<>|<\/>/},{className:"tag", + begin:n.concat(//,/>/,/\s/)))), + end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ + className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ + className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} + },grmr_yaml:e=>{ + const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ + className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ + },{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", + variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ + variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ + end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, + end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", + contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ + begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ + begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", + relevance:10},{className:"string", + begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ + begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, + relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", + begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t + },{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", + begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", + relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ + className:"number", + begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" + },{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] + ;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, + aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ + const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} + return He}() + ;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/app/src/main/assets/web/help/js/main.js b/app/src/main/assets/web/help/js/main.js new file mode 100755 index 000000000..1038503fd --- /dev/null +++ b/app/src/main/assets/web/help/js/main.js @@ -0,0 +1,41 @@ +require.config({ + baseUrl: 'js', + paths: { + marked: 'marked.min', + markedHighlight: 'marked-highlight.umd', + highlight: 'highlight.min', + }, + shim: { + marked: { + exports: 'marked', + }, + markedHighlight: { + exports: 'markedHighlight', + }, + highlight: { + exports: 'hljs', + }, + }, +}); + +require(['marked', 'markedHighlight', 'highlight'], (marked, mdhl, hljs) => { + marked.use( + mdhl.markedHighlight({ + langPrefix: 'theme-vs2015-min hljs language-', + highlight(code, lang) { + const language = hljs.getLanguage(lang) ? lang : 'txt'; + const result = hljs.highlight(code, {language}); + return result.value; + }, + }) + ); + + const path = '/help/md/'; + const file = location.hash.slice(1).trim(); + if (!file) return; + fetch(`${path}${file}.md`) + .then((response) => response.text()) + .then((md_text) => { + document.getElementById('mdviewer').innerHTML = marked.parse(md_text); + }); +}); diff --git a/app/src/main/assets/web/help/js/marked-highlight.umd.js b/app/src/main/assets/web/help/js/marked-highlight.umd.js new file mode 100755 index 000000000..591b3bf3b --- /dev/null +++ b/app/src/main/assets/web/help/js/marked-highlight.umd.js @@ -0,0 +1,3 @@ +(function(global,factory){typeof exports==='object'&&typeof module!=='undefined'?factory(exports):typeof define==='function'&&define.amd?define(['exports'],factory):(global=typeof globalThis!=='undefined'?globalThis:global||self,factory(global.markedHighlight={}))})(this,(function(exports){'use strict';function markedHighlight(options){if(typeof options==='function'){options={highlight:options}}if(!options||typeof options.highlight!=='function'){throw new Error('Must provide highlight function');}if(typeof options.langPrefix!=='string'){options.langPrefix='language-'}return{async:!!options.async,walkTokens(token){if(token.type!=='code'){return}const lang=getLang(token.lang);if(options.async){return Promise.resolve(options.highlight(token.text,lang,token.lang||'')).then(updateToken(token))}const code=options.highlight(token.text,lang,token.lang||'');if(code instanceof Promise){throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.');}updateToken(token)(code)},renderer:{code(code,infoString,escaped){const lang=getLang(infoString);const classAttr=`${options.langPrefix}${escape(lang||'txt')}`;code=code.replace(/\n$/,'');return` +
    ${escaped?code:escape(code,true)}\n${lang||'txt'}
    +`}}}}function getLang(lang){return(lang||'').match(/\S*/)[0]}function updateToken(token){return(code)=>{if(typeof code==='string'&&code!==token.text){token.escaped=true;token.text=code}}}const escapeTest=/[&<>"']/;const escapeReplace=new RegExp(escapeTest.source,'g');const escapeTestNoEncode=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;const escapeReplaceNoEncode=new RegExp(escapeTestNoEncode.source,'g');const escapeReplacements={'&':'&','<':'<','>':'>','"':'"',"'":'''};const getEscapeReplacement=(ch)=>escapeReplacements[ch];function escape(html,encode){if(encode){if(escapeTest.test(html)){return html.replace(escapeReplace,getEscapeReplacement)}}else{if(escapeTestNoEncode.test(html)){return html.replace(escapeReplaceNoEncode,getEscapeReplacement)}}return html}exports.markedHighlight=markedHighlight})); \ No newline at end of file diff --git a/app/src/main/assets/web/help/js/marked.min.js b/app/src/main/assets/web/help/js/marked.min.js new file mode 100755 index 000000000..2e66c369c --- /dev/null +++ b/app/src/main/assets/web/help/js/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v12.0.1 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
    '+(n?e:c(e,!0))+"
    \n":"
    "+(n?e:c(e,!0))+"
    \n"}blockquote(e){return`
    \n${e}
    \n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
    \n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); diff --git a/app/src/main/assets/web/help/js/require.js b/app/src/main/assets/web/help/js/require.js new file mode 100755 index 000000000..a4203f0d0 --- /dev/null +++ b/app/src/main/assets/web/help/js/require.js @@ -0,0 +1,5 @@ +/** vim: et:ts=4:sw=4:sts=4 + * @license RequireJS 2.3.6 Copyright jQuery Foundation and other contributors. + * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE + */ +var requirejs,require,define;!function(global,setTimeout){var req,s,head,baseElement,dataMain,src,interactiveScript,currentlyAddingScript,mainScript,subPath,version="2.3.6",commentRegExp=/\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/gm,cjsRequireRegExp=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,jsSuffixRegExp=/\.js$/,currDirRegExp=/^\.\//,op=Object.prototype,ostring=op.toString,hasOwn=op.hasOwnProperty,isBrowser=!("undefined"==typeof window||"undefined"==typeof navigator||!window.document),isWebWorker=!isBrowser&&"undefined"!=typeof importScripts,readyRegExp=isBrowser&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,defContextName="_",isOpera="undefined"!=typeof opera&&"[object Opera]"===opera.toString(),contexts={},cfg={},globalDefQueue=[],useInteractive=!1;function commentReplace(e,t){return t||""}function isFunction(e){return"[object Function]"===ostring.call(e)}function isArray(e){return"[object Array]"===ostring.call(e)}function each(e,t){var i;if(e)for(i=0;i name + "作者:" + author + - 导出文件名: + > Legado 是最好的在线阅读软件 作者: kunfei + +**【注】** *name、author 等变量与字符串的拼接都需要在 JSON 上下文环境中进行,即必须使用 `{}` 将变量与字符串包裹起来。* + +### 7. 为什么我打开本地的 TXT 文件,显示内容却是乱码? +部分编码在阅读上会识别错误,建议先用文本编辑器转换为常用的 UTF-8 格式。 + +### 8. 阅读对部分把正文(如所有含引号的句子)识别成标题,如何解决? +点击右上角更换目录规则即可。 + +## 书籍界面相关 + +### 1. 如何刷新书架? +在书架界面下拉即可刷新。 + +### 2. 书架界面书籍右上角的红色或者灰色背景小数字代表什么? +红色代表书籍有更新,灰色代表无更新,数字代表未读章节。 + +### 3. 如何查看书籍详情? +长按书籍即可查看。 + +### 4. 如何对书架上的书进行删除、切换书架的操作? +书籍详情页操作即可。 + +### 5. 如何禁止或允许某本书更新? +书籍详情页,点击右上角——“**允许更新**”。 + +### 6. 如何更换小说封面、名字、作者或简介? +书籍详情页,点击右上角修改按钮。 + +### 7. 怎么使用自定义字体? +阅读界面——“**字体**”——点击右上角选择字体文件路径。 + +### 8. 目前支持哪些格式的字体文件? +目前支持 TTF 和 OTF 格式。 + +### 9. 书籍经常“正在加载中”怎么办? +在线书籍出现这个问题通常是由于源质量不好或不兼容引起的,可以换其它源多试试;本地书籍出现这个问题大概率是目录规则问题,手动切换规则可以解决。 + +### 10. 书籍内容只有标题,正文内容是路径怎么办? +通常是缓存路径引起的,更换缓存路径即可。 + +### 11. 看书时如遇到“目录为空”、“加载失败”或长串英文等情况怎么办? +在线书籍一般是书源问题,切换或更新书源即可。本地书籍请尝试手动更换目录规则。 + +### 12. 为什么每一章的最后一页,阅读的文字和横线背景总是对不齐? +请在“**设置**”——“**文字底部对齐**”选项中关闭底部对齐,再调整排版。 + +### 13. 漫画源或图片章节只能看到第一页,如何解决? +请先查看原网页是否正常,若正常,请在书籍阅读界面点击右上角的“**⁝**”按钮,在弹出的菜单中,选择“**翻页动画(本书)**”,将翻页动画更改为“**滚动**”。 + +### 14. 阅读图片章节、漫画或 EPUB 插图时,图片被缩放到一页中,以至无法看清,如何处理? +* 临时处理方案:长按图片可以进行双指缩放。图片章节请先参考 Q13 中的方案将翻页动画更改为“**滚动**”。 +* 3.0 旧版可以点击书籍界面的章节标题进入“**编辑书源**”界面,在“**正文**”——“**图片样式**”中填入 *`full`*,保存更改,刷新当前章节即可。 +* 3.0 新版可以直接在书籍阅读界面点击右上角的“**⁝**”按钮,选择“**图片样式**”——***`full`***。 + + +## 替换净化相关 + +### 1. 替换净化是什么? +替换净化可以去除书籍内容里的广告、错别字、屏蔽词等。 + +### 2. 如何自己填写净化替换规则? +* 第一行:替换规则名称。请根据自己需求对替换净化规则进行命名; +* 第二行:分组。净化规则的分组组别; +* 第三行:替换规则。填写需要被替换的内容; +* 第四行:替换为。填写想替换成的内容(如不填则默认表示删除第三行里填写的内容); +* 第五行:替换范围,选填书名或者源名。填写此替换净化规则需要对哪本书籍或者哪个书源生效(如不填则对所有书籍和书源生效)。 + +**【注】** *如常规去除方法去除不掉,则需要勾选“使用正则表达式”,同时第三行里的替换规则也需要按照正则表达式来填写(正则表达式填写方法可自行网上搜索学习)。* + + +## 备份相关 + +### 1. 云备份在哪? +“**我的**”——“**备份与恢复**”——“**WebDav 设置**”。 + +### 2. 如何操作进行云备份? +* 侧栏设置,WebDav 设置; +* 正确填写 WebDAV 服务器地址、账号和密码; +* 无需操作,APP 默认每天自动云备份一次。 + +作者在此诚挚推荐使用【坚果云】进行 WebDav 备份。 + +如果直接在手机上注册,须下载【坚果云】APP,步骤较为繁琐。推荐在电脑上进行操作: +1. 打开注册链接:https://www.jianguoyun.com/d/signup ; +2. 注册后,进入坚果云; +3. 点击右上角账户名处选择“**账户信息**”,然后选择“**安全选项**”; +4. 在“**安全选项**”中找到“**第三方应用管理**”,并选择“**添加应用**”,输入名称(如“阅读”)后,会生成密码,选择完成; +5. 其中 `https://dav.jianguoyun.com/dav/` 就是填入“**WebDAV 服务器地址**”的内容,“**使用情况**”后面的邮箱地址就是你的“**WebDAV 账号**”,点击“**显示密码**“后得到的密码就是你的“**WebDAV 密码**”。 + +### 3. 关于云备份的相关说明 + +在正确设置好云备份的情况下,APP 默认每天自动云备份一次,当日多次手动云备份会对当日的旧云备份文件进行覆盖,并不会覆盖之前及之后不同日期的备份文件,每天所自动云备份的文件会按照日期进行命名。 + +### 4. 本地备份和云备份都能备份哪些东西? +书架、看书进度、搜索记录、书源、替换和 APP 设置等都会备份,基本涵盖所有内容。 + +### 5. 出现某些未知 Bug 怎么办? +清除软件数据试试看,不行再进行反馈。 + + +## 其他 + +### 1. 如何听书? +可以使用手机自带的朗读引擎,也可使用第三方如 Google(谷歌)或小米等朗读引擎。 + +【具体操作】*安装——系统设置——其他高级设置——辅助功能——TTS 输出——选择安装的朗读引擎(不同品牌手机的操作方法及步骤也不同,视情况而定)。* + +### 2. 如何设置屏幕方向、屏幕显示时长、显示/隐藏状态栏、显示/隐藏导航栏、音量键翻页、长按选择文本、点击总是翻下一页或自定义翻页按键? +阅读界面——“**设置**”(可上划,下面还有其他设置)。 + +### 3. 搜索的时候感觉手机卡顿,如何解决? +“**我的**”——“**其他设置**”——调低“**更新和搜索线程数**”。 diff --git a/app/src/main/assets/web/help/md/debugHelp.md b/app/src/main/assets/web/help/md/debugHelp.md new file mode 100755 index 000000000..dcfd3677e --- /dev/null +++ b/app/src/main/assets/web/help/md/debugHelp.md @@ -0,0 +1,22 @@ +# 书源调试 + +* 调试搜索>>输入关键字,如: +``` +系统 +``` +* 调试发现>>输入发现URL,如: +``` +月票榜::https://www.qidian.com/rank/yuepiao?page={{page}} +``` +* 调试详情页>>输入详情页URL,如: +``` +https://m.qidian.com/book/1015609210 +``` +* 调试目录页>>输入目录页URL,如: +``` +++https://www.zhaishuyuan.com/read/30394 +``` +* 调试正文页>>输入正文页URL,如: +``` +--https://www.zhaishuyuan.com/chapter/30394/20940996 +``` diff --git a/app/src/main/assets/web/help/md/dictRuleHelp.md b/app/src/main/assets/web/help/md/dictRuleHelp.md new file mode 100755 index 000000000..7574187b7 --- /dev/null +++ b/app/src/main/assets/web/help/md/dictRuleHelp.md @@ -0,0 +1,7 @@ +## 字典规则说明 + +* 字典规则是用在正文文字选择菜单字典里的规则,通常用来做翻译或者查找 +* urlRule + * 同书源的url规则 +* showRule + * 用来提取显示到对话框里面内容的规则 \ No newline at end of file diff --git a/app/src/main/assets/web/help/md/httpTTSHelp.md b/app/src/main/assets/web/help/md/httpTTSHelp.md new file mode 100755 index 000000000..16d9d6d87 --- /dev/null +++ b/app/src/main/assets/web/help/md/httpTTSHelp.md @@ -0,0 +1,15 @@ +# 在线朗读规则说明 + +* 在线朗读规则为url规则,同书源url +* js参数 +``` +speakText //朗读文本 +speakSpeed //朗读速度,5-50 +``` +* 例: +``` +http://tts.baidu.com/text2audio,{ + "method": "POST", + "body": "tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5003&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio" +} +``` \ No newline at end of file diff --git a/app/src/main/assets/web/help/md/jsHelp.md b/app/src/main/assets/web/help/md/jsHelp.md new file mode 100755 index 000000000..0460c53f9 --- /dev/null +++ b/app/src/main/assets/web/help/md/jsHelp.md @@ -0,0 +1,461 @@ +# js变量和函数 +> 阅读使用[Rhino v1.8.0](https://github.com/mozilla/rhino) 作为JavaScript引擎以便于[调用Java类和方法](https://m.jb51.net/article/92138.htm),查看[ECMAScript兼容性表格](https://mozilla.github.io/rhino/compat/engines.html) + +> [Rhino运行时](https://github.com/mozilla/rhino/blob/master/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java)懒加载导入的Java类和方法 + +|构造函数|函数|对象|调用类|简要说明| +|------|-----|------|----|------| +|JavaImporter|importClass importPackage| |[ImporterTopLevel](https://github.com/mozilla/rhino/blob/master/rhino/src/main/java/org/mozilla/javascript/ImporterTopLevel.java)|导入Java类到JavaScript| +||getClass|Packages java javax ...|[NativeJavaTopPackage](https://github.com/mozilla/rhino/blob/master/rhino/src/main/java/org/mozilla/javascript/NativeJavaTopPackage.java)|默认导入JavaScript中的Java类| +|JavaAdapter|||[JavaAdapter](https://github.com/mozilla/rhino/blob/master/rhino/src/main/java//org/mozilla/javascript/JavaAdapter.java)|继承Java类| + +> 注意`java`变量指向已经被阅读修改,如果想要调用`java.*`下的包,请使用`Packages.java.*` + +> 在书源规则中使用`@js` `` `{{}}`可使用JavaScript调用阅读部分内置的类和方法 + +> 注意为了安全,阅读会屏蔽部分java类调用,见[RhinoClassShutter](https://github.com/gedoor/legado/blob/master/modules/rhino/src/main/java/com/script/rhino/RhinoClassShutter.kt) + +> 不同的书源规则中支持的调用的Java类和方法可能有所不同 + +> 注意使用 `const` 声明的变量不支持块级作用域,在循环里使用会出现值不变的问题,请改用 `var` 声明 + +|变量名|调用类| +|------|-----| +|java|当前类| +|baseUrl|当前url,String | +|result|上一步的结果| +|book|[书籍类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)| +|rssArticle|[Article类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/RssArticle.kt)| +|chapter|[章节类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookChapter.kt)| +|source|[基础书源类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BaseSource.kt)| +|cookie|[cookie操作类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/http/CookieStore.kt)| +|cache|[缓存操作类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/CacheManager.kt)| +|title|章节当前标题 String| +|src| 请求返回的源码| +|nextChapterUrl|下一章节url| + +## 当前类对象的可使用的部分方法 + +### [RssJsExtensions](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt) +> 只能在订阅源`shouldOverrideUrlLoading`规则中使用 +> 订阅添加跳转url拦截, js, 返回true拦截,js变量url,可以通过js打开url +> url跳转拦截规则不能执行耗时操作 +> 例子https://github.com/gedoor/legado/discussions/3259 + +* 调用阅读搜索 + +```js +java.searchBook(bookName: String) +``` + +* 添加书架 + +```js +java.addBook(bookUrl: String) +``` + +### [AnalyzeUrl](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt) 部分函数 +> js中通过java.调用,只在`登录检查JS`规则中有效 +```js +initUrl() //重新解析url,可以用于登录检测js登录后重新解析url重新访问 +getHeaderMap().putAll(source.getHeaderMap(true)) //重新设置登录头 +getStrResponse( jsStr: String? = null, sourceRegex: String? = null) //返回访问结果,文本类型,书源内部重新登录后可调用此方法重新返回结果 +getResponse(): Response //返回访问结果,网络朗读引擎采用的是这个,调用登录后在调用这方法可以重新访问,参考阿里云登录检测 +``` + +### [AnalyzeRule](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt) 部分函数 +* 获取文本/文本列表 +> `mContent` 待解析源代码,默认为当前页面 +> `isUrl` 链接标识,默认为`false` +```js +java.getString(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false) +java.getStringList(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false) +``` +* 设置解析内容 + +```js +java.setContent(content: Any?, baseUrl: String? = null): +``` + +* 获取Element/Element列表 + +> 如果要改变解析源代码,请先使用`java.setContent` + +```js +java.getElement(ruleStr: String) +java.getElements(ruleStr: String) +``` + +* 重新搜索书籍/重新获取目录url + +> 只能在刷新目录之前使用,有些书源书籍地址和目录url会变 + +```js +java.reGetBook() +java.refreshTocUrl() +``` +* 变量存取 + +```js +java.get(key) +java.put(key, value) +``` + +### [js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt) 部分函数 + +* 链接解析[JsURL](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/utils/JsURL.kt) +```js +java.toURL(url): JsURL +java.toURL(url, baseUrl): JsURL +``` +* 获取SystemWebView User-Agent +```js +java.getWebViewUA(): String +``` +* 网络请求 +```js +java.ajax(urlStr): String +java.ajaxAll(urlList: Array): Array +//返回StrResponse 方法body() code() message() headers() raw() toString() +java.connect(urlStr): StrResponse + +java.post(url: String, body: String, headerMap: Map): Connection.Response + +java.get(url: String, headerMap: Map): Connection.Response + +java.head(url: String, headerMap: Map): Connection.Response + +* 使用webView访问网络 +* @param html 直接用webView载入的html, 如果html为空直接访问url +* @param url html内如果有相对路径的资源不传入url访问不了 +* @param js 用来取返回值的js语句, 没有就返回整个源代码 +* @return 返回js获取的内容 +java.webView(html: String?, url: String?, js: String?): String? + +* 使用webView获取跳转url +java.webViewGetOverrideUrl(html: String?, url: String?, js: String?, overrideUrlRegex: String): String? + +* 使用webView获取资源url +java.webViewGetSource(html: String?, url: String?, js: String?, sourceRegex: String): String? + +* 使用内置浏览器打开链接,可用于获取验证码 手动验证网站防爬 +* @param url 要打开的链接 +* @param title 浏览器的标题 +java.startBrowser(url: String, title: String) + +* 使用内置浏览器打开链接,并等待网页结果 .body()获取网页内容 +java.startBrowserAwait(url: String, title: String, refetchAfterSuccess: Boolean? = true): StrResponse +``` +* 调试 +```js +java.log(msg) +java.logType(var) +``` +* 获取用户输入的验证码 +```js +java.getVerificationCode(imageUrl) +``` +* 弹窗提示 +```js +java.longToast(msg: Any?) +java.toast(msg: Any?) +``` +* 从网络(由java.cacheFile实现)、本地读取JavaScript文件,导入上下文请手动`eval(String(...))` +```js +java.importScript(url) +//相对路径支持android/data/{package}/cache +java.importScript(relativePath) +java.importScript(absolutePath) +``` +* 缓存网络文件 +```js +获取 +java.cacheFile(url) +java.cacheFile(url,saveTime) +执行内容 +eval(String(java.cacheFile(url))) +使缓存失效 +cache.delete(java.md5Encode16(url)) +``` +* 获取网络压缩文件里面指定路径的数据 *可替换Zip Rar 7Z +```js +java.get*StringContent(url: String, path: String): String + +java.get*StringContent(url: String, path: String, charsetName: String): String + +java.get*ByteArrayContent(url: String, path: String): ByteArray? + +``` +* URI编码 +```js +java.encodeURI(str: String) //默认enc="UTF-8" +java.encodeURI(str: String, enc: String) +``` +* base64 +> flags参数可省略,默认Base64.NO_WRAP,查看[flags参数说明](https://blog.csdn.net/zcmain/article/details/97051870) +```js +java.base64Decode(str: String) +java.base64Decode(str: String, charset: String) +java.base64DecodeToByteArray(str: String, flags: Int) +java.base64Encode(str: String, flags: Int) +``` +* ByteArray +```js +Str转Bytes +java.strToBytes(str: String) +java.strToBytes(str: String, charset: String) +Bytes转Str +java.bytesToStr(bytes: ByteArray) +java.bytesToStr(bytes: ByteArray, charset: String) +``` +* Hex +```js +HexString 解码为字节数组 +java.hexDecodeToByteArray(hex: String) +hexString 解码为utf8String +java.hexDecodeToString(hex: String) +utf8 编码为hexString +java.hexEncodeToString(utf8: String) +``` +* 标识id +```js +java.randomUUID() +java.androidId() +``` +* 繁简转换 +```js +将文本转换为简体 +java.t2s(text: String): String +将文本转换为繁体 +java.s2t(text: String): String +``` +* 时间格式化 +```js +java.timeFormatUTC(time: Long, format: String, sh: Int): String? +java.timeFormat(time: Long): String +``` +* html格式化 +```js +java.htmlFormat(str: String): String +``` +* 文件 +> 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存/android/data/{package}/cache/内的文件 +```js +//文件下载 url用于生成文件名,返回文件路径 +downloadFile(url: String): String +//文件解压,zipPath为压缩文件路径,返回解压路径 +unArchiveFile(zipPath: String): String +unzipFile(zipPath: String): String +unrarFile(zipPath: String): String +un7zFile(zipPath: String): String +//文件夹内所有文件读取 +getTxtInFolder(unzipPath: String): String +//读取文本文件 +readTxtFile(path: String): String +//删除文件 +deleteFile(path: String) +``` + +### [js加解密类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsEncodeUtils.kt) 部分函数 + +> 提供在JavaScript环境中快捷调用crypto算法的函数,由[hutool-crypto](https://www.hutool.cn/docs/#/crypto/概述)实现 +> 由于兼容性问题,hutool-crypto当前版本为5.8.22 + +> 注意:如果输入的参数不是Utf8String 可先调用`java.hexDecodeToByteArray java.base64DecodeToByteArray`转成ByteArray +* 对称加密 +> 输入参数key iv 支持ByteArray|**Utf8String** +```js +// 创建Cipher +java.createSymmetricCrypto(transformation, key, iv) +``` +>解密加密参数 data支持ByteArray|Base64String|HexString|InputStream +```js +//解密为ByteArray String +cipher.decrypt(data) +cipher.decryptStr(data) +//加密为ByteArray Base64字符 HEX字符 +cipher.encrypt(data) +cipher.encryptBase64(data) +cipher.encryptHex(data) +``` +* 非对称加密 +> 输入参数 key支持ByteArray|**Utf8String** +```js +//创建cipher +java.createAsymmetricCrypto(transformation) +//设置密钥 +.setPublicKey(key) +.setPrivateKey(key) + +``` +> 解密加密参数 data支持ByteArray|Base64String|HexString|InputStream +```js +//解密为ByteArray String +cipher.decrypt(data, usePublicKey: Boolean? = true +) +cipher.decryptStr(data, usePublicKey: Boolean? = true +) +//加密为ByteArray Base64字符 HEX字符 +cipher.encrypt(data, usePublicKey: Boolean? = true +) +cipher.encryptBase64(data, usePublicKey: Boolean? = true +) +cipher.encryptHex(data, usePublicKey: Boolean? = true +) +``` +* 签名 +> 输入参数 key 支持ByteArray|**Utf8String** +```js +//创建Sign +java.createSign(algorithm) +//设置密钥 +.setPublicKey(key) +.setPrivateKey(key) +``` +> 签名参数 data支持ByteArray|inputStream|String +```js +//签名输出 ByteArray HexString +sign.sign(data) +sign.signHex(data) +``` +* 摘要 +```js +java.digestHex(data: String, algorithm: String,): String? + +java.digestBase64Str(data: String, algorithm: String,): String? +``` +* md5 +```js +java.md5Encode(str) +java.md5Encode16(str) +``` +* HMac +```js +java.HMacHex(data: String, algorithm: String, key: String): String + +java.HMacBase64(data: String, algorithm: String, key: String): String +``` + +## book对象的可用属性 +### 属性 +> 使用方法: 在js中或{{}}中使用book.属性的方式即可获取.如在正文内容后加上 ##{{book.name+"正文卷"+title}} 可以净化 书名+正文卷+章节名称(如 我是大明星正文卷第二章我爸是豪门总裁) 这一类的字符. +```js +bookUrl // 详情页Url(本地书源存储完整文件路径) +tocUrl // 目录页Url (toc=table of Contents) +origin // 书源URL(默认BookType.local) +originName //书源名称 or 本地书籍文件名 +name // 书籍名称(书源获取) +author // 作者名称(书源获取) +kind // 分类信息(书源获取) +customTag // 分类信息(用户修改) +coverUrl // 封面Url(书源获取) +customCoverUrl // 封面Url(用户修改) +intro // 简介内容(书源获取) +customIntro // 简介内容(用户修改) +charset // 自定义字符集名称(仅适用于本地书籍) +type // 0:text 1:audio +group // 自定义分组索引号 +latestChapterTitle // 最新章节标题 +latestChapterTime // 最新章节标题更新时间 +lastCheckTime // 最近一次更新书籍信息的时间 +lastCheckCount // 最近一次发现新章节的数量 +totalChapterNum // 书籍目录总数 +durChapterTitle // 当前章节名称 +durChapterIndex // 当前章节索引 +durChapterPos // 当前阅读的进度(首行字符的索引位置) +durChapterTime // 最近一次阅读书籍的时间(打开正文的时间) +canUpdate // 刷新书架时更新书籍信息 +order // 手动排序 +originOrder //书源排序 +variable // 自定义书籍变量信息(用于书源规则检索书籍信息) + ``` + +## chapter对象的部分可用属性 +> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符. + ```js + url // 章节地址 + title // 章节标题 + baseUrl //用来拼接相对url + bookUrl // 书籍地址 + index // 章节序号 + resourceUrl // 音频真实URL + tag // + start // 章节起始位置 + end // 章节终止位置 + variable //变量 + ``` + +## source对象的部分可用函数 +* 获取书源url +```js +source.getKey() +``` +* 书源变量存取 +```js +source.setVariable(variable: String?) +source.getVariable() +``` + +* 登录头操作 +```js +获取登录头 +source.getLoginHeader() +获取登录头某一键值 +source.getLoginHeaderMap().get(key: String) +保存登录头 +source.putLoginHeader(header: String) +清除登录头 +source.removeLoginHeader() +``` +* 用户登录信息操作 +> 使用`登录UI`规则,并成功登录,阅读自动加密保存登录UI规则中除type为button的信息 +```js +login函数获取登录信息 +source.getLoginInfo() +login函数获取登录信息键值 +source.getLoginInfoMap().get(key: String) +清除登录信息 +source.removeLoginInfo() +``` +## cookie对象的部分可用函数 +```js +获取全部cookie +cookie.getCookie(url) +获取cookie某一键值 +cookie.getKey(url,key) +设置cookie +cookie.setCookie(url,cookie) +替换cookie +cookie.replaceCookie(url,cookie) +删除cookie +cookie.removeCookie(url) +``` + +## cache对象的部分可用函数 +> saveTime单位:秒,可省略 +> 保存至数据库和缓存文件(50M),保存的内容较大时请使用`getFile putFile` +```js +保存 +cache.put(key: String, value: String, saveTime: Int) +读取数据库 +cache.get(key: String): String? +删除 +cache.delete(key: String) +缓存文件内容 +cache.putFile(key: String, value: String, saveTime: Int) +读取文件内容 +cache.getFile(key: String): String? +保存到内存 +cache.putMemory(key: String, value: Any) +读取内存 +cache.getFromMemory(key: String): Any? +删除内存 +cache.deleteMemory(key: String) +``` + +## 跳转外部链接/应用函数 +```js +// 跳转外部链接,传入http链接或者scheme跳转到浏览器或其他应用 +java.openUrl(url:String) +// 指定mimeType,可以跳转指定类型应用,例如(video/*) +java.openUrl(url:String,mimeType:String) \ No newline at end of file diff --git a/app/src/main/assets/web/help/md/readMenuHelp.md b/app/src/main/assets/web/help/md/readMenuHelp.md new file mode 100755 index 000000000..33a3c632e --- /dev/null +++ b/app/src/main/assets/web/help/md/readMenuHelp.md @@ -0,0 +1,46 @@ +# 阅读界面帮助文档 + +## 阅读界面主菜单 +* 顶部操作 + * 章节名称:点击可编辑书源 + * 章节url:点击可打开浏览器浏览 + * 菜单:**不同类型的书籍显示的菜单不同**。详情请查看菜单文字,长按菜单图标可显示文字 +* 中间左侧-亮度调节 + * 亮度调节的顶端有跟随系统亮度的开关,打开后亮度跟随系统,关闭后才可以调节亮度条 +* 底部操作 + * 4个圆形按钮依次为 全文搜索✧自动翻页✧替换净化✧切换夜间模式 + * 上一章✧下一章中间的进度条为页数进度,要快速跳转章节点击目录按钮进入目录快速跳转 + * 目录->目录和书签界面 + * 朗读->单击开始朗读,长按进入朗读设置界面 + * 界面->所有排版设置都在里面 + * 设置->其它一些设置,找不到的设置去这里看看,可滚动 + +## 全文搜索 +搜索本地缓存或者本地文件,不能搜索在线内容 +书籍字数、净化规则数量、简繁转化、文章分段都会影响到搜索速度,请酌情启用 + +## 朗读设置界面 +* 后台->进入后台朗读,可以做一些其它事 +* 设置->朗读引擎设置,可以切换本地TTS和在线朗读,在线朗读可自定义 + +## 排版设置界面 +* 白天模式和夜间模式背景不同布局相同 +* 共用布局->启用共用布局时所有背景使用同一布局,关闭共用布局则每个背景单独布局 +* 长按背景可进入文字颜色和背景设置界面 + +## 其它设置界面 +* 屏幕方向 +* 屏幕超时 +* 隐藏状态栏 +* 扩展到刘海 +* 隐藏导航栏 +* 文字两端对齐 +* 文字底部对齐 +* 音量键翻页 +* 点击翻页 +* 朗读时音量键翻页 +* 自动换源->书源被删除时自动切换到其它书源 +* 长按选择文本 +* 显示亮度调节控件 +* 点击区域设置 +* 自定义翻页按键 diff --git a/app/src/main/assets/web/help/md/regexHelp.md b/app/src/main/assets/web/help/md/regexHelp.md new file mode 100755 index 000000000..cf0779d80 --- /dev/null +++ b/app/src/main/assets/web/help/md/regexHelp.md @@ -0,0 +1,204 @@ +# 正则表达式学习 + +- [基本匹配] +- [元字符] + - [英文句号] + - [字符集] + - [否定字符集] + - [重复] + - [星号] + - [加号] + - [问号] + - [花括号] + - [字符组] + - [分支结构] + - [转义特殊字符] + - [定位符] + - [插入符号] + - [美元符号] +- [简写字符集] +- [断言] + - [正向先行断言] + - [负向先行断言] + - [正向后行断言] + - [负向后行断言] +- [标记] + - [不区分大小写] + - [全局搜索] + - [多行匹配] +- [常用正则表达式] + +## 1. 基本匹配 + +正则表达式只是我们用于在文本中检索字母和数字的模式。例如正则表达式 `cat`,表示: 字母 `c` 后面跟着一个字母 `a`,再后面跟着一个字母 `t`。
    "cat" => The cat sat on the mat
    + +正则表达式 `123` 会匹配字符串 "123"。通过将正则表达式中的每个字符逐个与要匹配的字符串中的每个字符进行比较,来完成正则匹配。 +正则表达式通常区分大小写,因此正则表达式 `Cat` 与字符串 "cat" 不匹配。
    "Cat" => The cat sat on the Cat
    + +## 2. 元字符 + +元字符是正则表达式的基本组成元素。元字符在这里跟它通常表达的意思不一样,而是以某种特殊的含义去解释。有些元字符写在方括号内的时候有特殊含义。 +元字符如下: + +|元字符|描述| +|:----:|----| +|.|匹配除换行符以外的任意字符。| +|[ ]|字符类,匹配方括号中包含的任意字符。| +|[^ ]|否定字符类。匹配方括号中不包含的任意字符| +|*|匹配前面的子表达式零次或多次| +|+|匹配前面的子表达式一次或多次| +|?|匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。| +|{n,m}|花括号,匹配前面字符至少 n 次,但是不超过 m 次。| +|(xyz)|字符组,按照确切的顺序匹配字符xyz。| +|||分支结构,匹配符号之前的字符或后面的字符。| +|\|转义符,它可以还原元字符原来的含义,允许你匹配保留字符 [ ] ( ) { } . * + ? ^ $ \ || +|^|匹配行的开始| +|$|匹配行的结束| + +## 2.1 英文句号 + +英文句号 `.` 是元字符的最简单的例子。元字符 `.` 可以匹配任意单个字符。它不会匹配换行符和新行的字符。例如正则表达式 `.ar`,表示: 任意字符后面跟着一个字母 `a`, +再后面跟着一个字母 `r`。
    ".ar" => The car parked in the garage.
    + +## 2.2 字符集 + +字符集也称为字符类。方括号被用于指定字符集。使用字符集内的连字符来指定字符范围。方括号内的字符范围的顺序并不重要。 +例如正则表达式 `[Tt]he`,表示: 大写 `T` 或小写 `t` ,后跟字母 `h`,再后跟字母 `e`。
    "[Tt]he" => The car parked in the garage.
    + +然而,字符集中的英文句号表示它字面的含义。正则表达式 `ar[.]`,表示小写字母 `a`,后面跟着一个字母 `r`,再后面跟着一个英文句号 `.` 字符。
    "ar[.]" => A garage is a good place to park a car.
    + +### 2.2.1 否定字符集 + +一般来说插入字符 `^` 表示一个字符串的开始,但是当它在方括号内出现时,它会取消字符集。例如正则表达式 `[^c]ar`,表示: 除了字母 `c` 以外的任意字符,后面跟着字符 `a`, +再后面跟着一个字母 `r`。
    "[^c]ar" => The car parked in the garage.
    + +## 2.3 重复 + +以下元字符 `+`,`*` 或 `?` 用于指定子模式可以出现多少次。这些元字符在不同情况下的作用不同。 + +### 2.3.1 星号 + +该符号 `*` 表示匹配上一个匹配规则的零次或多次。正则表达式 `a*` 表示小写字母 `a` 可以重复零次或者多次。但是它如果出现在字符集或者字符类之后,它表示整个字符集的重复。 +例如正则表达式 `[a-z]*`,表示: 一行中可以包含任意数量的小写字母。
    "[a-z]*" => The car parked in the garage #21.
    + +该 `*` 符号可以与元符号 `.` 用在一起,用来匹配任意字符串 `.*`。该 `*` 符号可以与空格符 `\s` 一起使用,用来匹配一串空格字符。 +例如正则表达式 `\s*cat\s*`,表示: 零个或多个空格,后面跟小写字母 `c`,再后面跟小写字母 `a`,再再后面跟小写字母 `t`,后面再跟零个或多个空格。
    "\s*cat\s*" => The fat cat sat on the cat.
    + +### 2.3.2 加号 + +该符号 `+` 匹配上一个字符的一次或多次。例如正则表达式 `c.+t`,表示: 一个小写字母 `c`,后跟任意数量的字符,后跟小写字母 `t`。
    "c.+t" => The fat cat sat on the mat.
    + +### 2.3.3 问号 + +在正则表达式中,元字符 `?` 用来表示前一个字符是可选的。该符号匹配前一个字符的零次或一次。 +例如正则表达式 `[T]?he`,表示: 可选的大写字母 `T`,后面跟小写字母 `h`,后跟小写字母 `e`。
    "[T]he" => The car is parked in the garage.
    "[T]?he" => The car is parked in the garage.
    + +## 2.4 花括号 + +在正则表达式中花括号(也被称为量词 ?)用于指定字符或一组字符可以重复的次数。例如正则表达式 `[0-9]{2,3}`,表示: 匹配至少2位数字但不超过3位(0到9范围内的字符)。
    "[0-9]{2,3}" => The number was 9.9997 but we rounded it off to 10.0.
    + +我们可以省略第二个数字。例如正则表达式 `[0-9]{2,}`,表示: 匹配2个或更多个数字。如果我们也删除逗号,则正则表达式 `[0-9]{2}`,表示: 匹配正好为2位数的数字。
    "[0-9]{2,}" => The number was 9.9997 but we rounded it off to 10.0.
    "[0-9]{2}" => The number was 9.9997 but we rounded it off to 10.0.
    + +## 2.5 字符组 + +字符组是一组写在圆括号内的子模式 `(...)`。正如我们在正则表达式中讨论的那样,如果我们把一个量词放在一个字符之后,它会重复前一个字符。 +但是,如果我们把量词放在一个字符组之后,它会重复整个字符组。 +例如正则表达式 `(ab)*` 表示匹配零个或多个的字符串 "ab"。我们还可以在字符组中使用元字符 `|`。例如正则表达式 `(c|g|p)ar`,表示: 小写字母 `c`、`g` 或 `p` 后面跟字母 `a`,后跟字母 `r`。
    "(c|g|p)ar" => The car is parked in the garage.
    + +## 2.6 分支结构 + +在正则表达式中垂直条 `|` 用来定义分支结构,分支结构就像多个表达式之间的条件。现在你可能认为这个字符集和分支机构的工作方式一样。 +但是字符集和分支结构巨大的区别是字符集只在字符级别上有作用,然而分支结构在表达式级别上依然可以使用。 +例如正则表达式 `(T|t)he|car`,表示: 大写字母 `T` 或小写字母 `t`,后面跟小写字母 `h`,后跟小写字母 `e` 或小写字母 `c`,后跟小写字母 `a`,后跟小写字母 `r`。
    "(T|t)he|car" => The car is parked in the garage.
    + +## 2.7 转义特殊字符 + +正则表达式中使用反斜杠 `\` 来转义下一个字符。这将允许你使用保留字符来作为匹配字符 `{ } [ ] / \ + * . $ ^ | ?`。在特殊字符前面加 `\`,就可以使用它来做匹配字符。 +例如正则表达式 `.` 是用来匹配除了换行符以外的任意字符。现在要在输入字符串中匹配 `.` 字符,正则表达式 `(f|c|m)at\.?`,表示: 小写字母 `f`、`c` 或者 `m` 后跟小写字母 `a`,后跟小写字母 `t`,后跟可选的 `.` 字符。
    "(f|c|m)at\.?" => The fat cat sat on the mat.
    + +## 2.8 定位符 + +在正则表达式中,为了检查匹配符号是否是起始符号或结尾符号,我们使用定位符。 +定位符有两种类型: 第一种类型是 `^` 检查匹配字符是否是起始字符,第二种类型是 `$`,它检查匹配字符是否是输入字符串的最后一个字符。 + +### 2.8.1 插入符号 + +插入符号 `^` 符号用于检查匹配字符是否是输入字符串的第一个字符。如果我们使用正则表达式 `^a` (如果a是起始符号)匹配字符串 `abc`,它会匹配到 `a`。 +但是如果我们使用正则表达式 `^b`,它是匹配不到任何东西的,因为在字符串 `abc` 中 "b" 不是起始字符。 +让我们来看看另一个正则表达式 `^(T|t)he`,这表示: 大写字母 `T` 或小写字母 `t` 是输入字符串的起始符号,后面跟着小写字母 `h`,后跟小写字母 `e`。
    "(T|t)he" => The car is parked in the garage.
    "^(T|t)he" => The car is parked in the garage.
    + +### 2.8.2 美元符号 + +美元 `$` 符号用于检查匹配字符是否是输入字符串的最后一个字符。例如正则表达式 `(at\.)$`,表示: 小写字母 `a`,后跟小写字母 `t`,后跟一个 `.` 字符,且这个匹配器必须是字符串的结尾。
    "(at\.)" => The fat cat. sat. on the mat.
    "(at\.)$" => The fat cat sat on the mat.
    + +## 3. 简写字符集 + +正则表达式为常用的字符集和常用的正则表达式提供了简写。简写字符集如下: + +|简写|描述| +|:----:|----| +|.|匹配除换行符以外的任意字符| +|\w|匹配所有字母和数字的字符: `[a-zA-Z0-9_]`| +|\W|匹配非字母和数字的字符: `[^\w]`| +|\d|匹配数字: `[0-9]`| +|\D|匹配非数字: `[^\d]`| +|\s|匹配空格符: `[\t\n\f\r\p{Z}]`| +|\S|匹配非空格符: `[^\s]`| + +## 4. 断言 + +后行断言和先行断言有时候被称为断言,它们是特殊类型的 ***非捕获组*** (用于匹配模式,但不包括在匹配列表中)。当我们在一种特定模式之前或者之后有这种模式时,会优先使用断言。 +例如我们想获取输入字符串 `$4.44 and $10.88` 中带有前缀 `$` 的所有数字。我们可以使用这个正则表达式 `(?<=\$)[0-9\.]*`,表示: 获取包含 `.` 字符且前缀为 `$` 的所有数字。 +以下是正则表达式中使用的断言: + +|符号|描述| +|:----:|----| +|?=|正向先行断言| +|?!|负向先行断言| +|?<=|正向后行断言| +|?"(T|t)he(?=\sfat)" => The fat cat sat on the mat. + +### 4.2 负向先行断言 + +当我们需要从输入字符串中获取不匹配表达式的内容时,使用负向先行断言。负向先行断言的定义跟我们定义的正向先行断言一样, +唯一的区别是不是等号 `=`,我们使用否定符号 `!`,例如 `(?!...)`。 +我们来看看下面的正则表达式 `(T|t)he(?!\sfat)`,表示: 从输入字符串中获取全部 `The` 或者 `the` 且不匹配 `fat` 前面加上一个空格字符。
    "(T|t)he(?!\sfat)" => The fat cat sat on the mat.
    + +### 4.3 正向后行断言 + +正向后行断言是用于获取在特定模式之前的所有匹配内容。正向后行断言表示为 `(?<=...)`。例如正则表达式 `(?<=(T|t)he\s)(fat|mat)`,表示: 从输入字符串中获取在单词 `The` 或 `the` 之后的所有 `fat` 和 `mat` 单词。
    "(?<=(T|t)he\s)(fat|mat)" => The fat cat sat on the mat.
    + +### 4.4 负向后行断言 + +负向后行断言是用于获取不在特定模式之前的所有匹配的内容。负向后行断言表示为 `(?"(?<!(T|t)he\s)(cat)" => The cat sat on cat. + +## 5. 标记 + +标记也称为修饰符,因为它会修改正则表达式的输出。这些标志可以以任意顺序或组合使用,并且是正则表达式的一部分。 + +|标记|描述| +|:----:|----| +|i|不区分大小写: 将匹配设置为不区分大小写。| +|g|全局搜索: 搜索整个输入字符串中的所有匹配。| +|m|多行匹配: 会匹配输入字符串每一行。| + +* **数字**: `\d+$` +* **用户名**: `^[\w\d_.]{4,16}$` +* **字母数字字符**: `^[a-zA-Z0-9]*$` +* **带空格的字母数字字符**: `^[a-zA-Z0-9 ]*$` +* **小写字母**: `[a-z]+$` +* **大写字母**: `[A-Z]+$` +* **网址**: `^(((http|https|ftp):\/\/)?([[a-zA-Z0-9]\-\.])+(\.)([[a-zA-Z0-9]]){2,4}([[a-zA-Z0-9]\/+=%&_\.~?\-]*))*$` +* **日期 (MM/DD/YYYY)**: `^(0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])[- /.](19|20)?[0-9]{2}$` +* **日期 (YYYY/MM/DD)**: `^(19|20)?[0-9]{2}[- /.](0?[1-9]|1[012])[- /.](0?[1-9]|[12][0-9]|3[01])$` +* **求更求转发致谢**: `[\((【].*?[求更谢乐发推].*?[】)\)]` +* **查找最新章节**: `您可以.*?查找最新章节` +* **ps/PS**: `(?i)ps\b.*` +* **Html标签**: `<[^>]+?>` diff --git a/app/src/main/assets/web/help/md/replaceRuleHelp.md b/app/src/main/assets/web/help/md/replaceRuleHelp.md new file mode 100755 index 000000000..063c8d3a3 --- /dev/null +++ b/app/src/main/assets/web/help/md/replaceRuleHelp.md @@ -0,0 +1,6 @@ +# 替换管理界面帮助 + +* 替换规则是用来替换正文内容的一种规则 + * 菜单可以新建和导入规则 + * 可以拖动排序 + * 可以选择操作 \ No newline at end of file diff --git a/app/src/main/assets/web/help/md/ruleHelp.md b/app/src/main/assets/web/help/md/ruleHelp.md new file mode 100755 index 000000000..80f578e91 --- /dev/null +++ b/app/src/main/assets/web/help/md/ruleHelp.md @@ -0,0 +1,246 @@ +# 源规则帮助 + +* [阅读3.0(Legado)规则说明](https://mgz0227.github.io/The-tutorial-of-Legado/) +* [书源帮助文档](https://mgz0227.github.io/The-tutorial-of-Legado/Rule/source.html) +* [订阅源帮助文档](https://mgz0227.github.io/The-tutorial-of-Legado/Rule/rss.html) +* 辅助键盘❓中可插入URL参数模板,打开帮助,js教程,正则教程,选择文件 +* 规则标志, {{......}}内使用规则必须有明显的规则标志,没有规则标志当作js执行 +``` +@@ 默认规则,直接写时可以省略@@ +@XPath: xpath规则,直接写时以//开头可省略@XPath +@Json: json规则,直接写时以$.开头可省略@Json +: regex规则,不可省略,只可以用在书籍列表和目录列表 +``` +* jsLib +> 注入JavaScript到RhinoJs引擎中,支持两种格式,可实现[函数共用](https://github.com/gedoor/legado/wiki/JavaScript%E5%87%BD%E6%95%B0%E5%85%B1%E7%94%A8) + +> `JavaScript Code` 直接填写JavaScript片段 +> `{"example":"https://www.example.com/js/example.js", ...}` 自动复用已经下载的js文件 + +> 注意此处定义的函数可能会被多个线程同时调用,在函数里的全局变量内容将会共享使用,对其进行修改可能会出现竞争问题 +> 函数内不可声明全局变量,函数外的全局变量不可再赋值,否则会抛出 `无法修改密封对象的属性` 异常 + +* 并发率 +> 并发限制,单位ms,可填写两种格式 + +> `1000` 访问间隔1s +> `20/60000` 60s内访问次数20 + +* 书源类型: 文件 +> 对于类似知轩藏书提供文件整合下载的网站,可以在书源详情的下载URL规则获取文件链接 + +> 通过截取下载链接或文件响应头头获取文件信息,获取失败会自动拼接`书名` `作者`和下载链接的`UrlOption`的`type`字段 + +> 压缩文件解压缓存会在下次启动后自动清理,不会占用额外空间 + +* CookieJar +> 启用后会自动保存每次返回头中的Set-Cookie中的值,适用于验证码图片一类需要session的网站 + +* 登录UI +> 不使用内置webView登录网站,需要使用`登录URL`规则实现登录逻辑,可使用`登录检查JS`检查登录结果 +> 版本20221113重要更改:按钮支持调用`登录URL`规则里面的函数,必须实现`login`函数 +``` +规则填写示范 +[ + { + "name": "telephone", + "type": "text" + }, + { + "name": "password", + "type": "password" + }, + { + "name": "注册", + "type": "button", + "action": "http://www.yooike.com/xiaoshuo/#/register?title=%E6%B3%A8%E5%86%8C" + }, + { + "name": "获取验证码", + "type": "button", + "action": "getVerificationCode()", + "style": { + "layout_flexGrow": 0, + "layout_flexShrink": 1, + "layout_alignSelf": "auto", + "layout_flexBasisPercent": -1, + "layout_wrapBefore": false + } + } +] +``` +* 登录URL +> 可填写登录链接或者实现登录UI的登录逻辑的JavaScript +``` +示范填写 +function login() { + java.log("模拟登录请求"); + java.log(source.getLoginInfoMap()); +} +function getVerificationCode() { + java.log("登录UI按钮:获取到手机号码"+result.get("telephone")) +} + +登录按钮函数获取登录信息 +result.get("telephone") +login函数获取登录信息 +source.getLoginInfo() +source.getLoginInfoMap().get("telephone") +source登录相关方法,可在js内通过source.调用,可以参考阿里云语音登录 +login() +getHeaderMap(hasLoginHeader: Boolean = false) +getLoginHeader(): String? +getLoginHeaderMap(): Map? +putLoginHeader(header: String) +removeLoginHeader() +setVariable(variable: String?) +getVariable(): String? +AnalyzeUrl相关函数,js中通过java.调用 +initUrl() //重新解析url,可以用于登录检测js登录后重新解析url重新访问 +getHeaderMap().putAll(source.getHeaderMap(true)) //重新设置登录头 +getStrResponse( jsStr: String? = null, sourceRegex: String? = null) //返回访问结果,文本类型,书源内部重新登录后可调用此方法重新返回结果 +getResponse(): Response //返回访问结果,网络朗读引擎采用的是这个,调用登录后在调用这方法可以重新访问,参考阿里云登录检测 +``` + +* 发现url格式 +```json +[ + { + "title": "xxx", + "url": "", + "style": { + "layout_flexGrow": 0, + "layout_flexShrink": 1, + "layout_alignSelf": "auto", + "layout_flexBasisPercent": -1, + "layout_wrapBefore": false + } + } +] +``` + +* 请求头,支持http代理,socks4 socks5代理设置 +> 注意请求头的key是区分大小写的 +> 正确格式 User-Agent Referer +> 错误格式 user-agent referer +``` +socks5代理 +{ + "proxy":"socks5://127.0.0.1:1080" +} +不支持需要验证的socks代理 +http代理 +{ + "proxy":"http://127.0.0.1:1080" +} +支持http代理服务器验证 +{ + "proxy":"http://127.0.0.1:1080@用户名@密码" +} +注意:这些请求头是无意义的,会被忽略掉 +``` + +* url添加js参数,解析url时执行,可在访问url时处理url,例 +``` +https://www.baidu.com,{"js":"java.headerMap.put('xxx', 'yyy')"} +https://www.baidu.com,{"js":"java.url=java.url+'yyyy'"} +``` + +* 增加js方法,用于重定向拦截 + * `java.get(urlStr: String, headers: Map)` + * `java.post(urlStr: String, body: String, headers: Map)` +* 对于搜索重定向的源,可以使用此方法获得重定向后的url +``` +(()=>{ + if(page==1){ + let url='https://www.yooread.net/e/search/index.php,'+JSON.stringify({ + "method":"POST", + "body":"show=title&tempid=1&keyboard="+key + }); + return source.put('surl',String(java.connect(url).raw().request().url())); + } else { + return source.get('surl')+'&page='+(page-1) + } +})() +或者 +(()=>{ + let base='https://www.yooread.net/e/search/'; + if(page==1){ + let url=base+'index.php'; + let body='show=title&tempid=1&keyboard='+key; + return base+source.put('surl',java.post(url,body,{}).header("Location")); + } else { + return base+source.get('surl')+'&page='+(page-1); + } +})() +``` + +* 图片链接支持修改headers +``` +let options = { +"headers": {"User-Agent": "xxxx","Referrer":baseUrl,"Cookie":"aaa=vbbb;"} +}; +'' +``` + +* 字体解析使用 +> 使用方法,在正文替换规则中使用,原理根据f1字体的字形数据到f2中查找字形对应的编码 +``` + +(function(){ + var b64=String(src).match(/ttf;base64,([^\)]+)/); + if(b64){ + var f1 = java.queryTTF(b64[1]); + var f2 = java.queryTTF("https://alanskycn.gitee.io/teachme/assets/font/Source Han Sans CN Regular.ttf"); + // return java.replaceFont(result, f1, f2); + return java.replaceFont(result, f1, f2, true); // 过滤掉f1中不存在的字形 + } + return result; +})() + +``` + +* 购买操作 +> 可直接填写链接或者JavaScript,如果执行结果是网络链接将会自动打开浏览器,js返回true自动刷新目录和当前章节 + +* 图片解密 +> 适用于图片需要二次解密的情况,直接填写JavaScript,返回解密后的`ByteArray` +> 部分变量说明:java(仅支持[js扩展类](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/help/JsExtensions.kt)),result为待解密图片的`ByteArray`,src为图片链接 + +```js +java.createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv).decrypt(result) +``` + +```js +function decodeImage(data, key) { + var input = new Packages.java.io.ByteArrayInputStream(data) + var out = new Packages.java.io.ByteArrayOutputStream() + var byte + while ((byte = input.read()) != -1) { + out.write(byte ^ key) + } + return out.toByteArray() +} + +decodeImage(result, key) +``` + +* 封面解密 +> 同图片解密 其中result为待解密封面的`inputStream` + +```js +java.createSymmetricCrypto("AES/CBC/PKCS5Padding", key, iv).decrypt(result) +``` + +```js +function decodeImage(data, key) { + var out = new Packages.java.io.ByteArrayOutputStream() + var byte + while ((byte = data.read()) != -1) { + out.write(byte ^ key) + } + return out.toByteArray() +} + +decodeImage(result, key) +``` diff --git a/app/src/main/assets/web/help/md/txtTocRuleHelp.md b/app/src/main/assets/web/help/md/txtTocRuleHelp.md new file mode 100755 index 000000000..7f09bc9c0 --- /dev/null +++ b/app/src/main/assets/web/help/md/txtTocRuleHelp.md @@ -0,0 +1,16 @@ +## Txt目录正则说明 + +### 菜单区 + +- 新增目录规则,当Legado自带的规则不能够满足需求时,用户可根据自己的情况自定义目录规则 +- 导入默认规则 在旧版本中,Legado自带的规则不会随着软件的更新而更新。用户需要使用最新规则或对自带规则修改后需要重置时,可点击 导入默认规则。**注意:导入默认规则不会重置用户自定义的规则,但如果您对自带的规则进行了修改,则修改的规则会被重置为默认规则。** +- 网络导入 为了方便异步调试以及用户导入他人的目录规则,Legado增加目录规则的网络导入功能。点击 网络导入 的输入框,可以通过内置的链接导入在线规则。(在线规则优先比内置的规则更加激进,但适配了更多类型的标题格式,用户需根据自己的情况选择是否导入) +- 拆分超长章节 根据目录正则分章时若单章超过三万字左右将自动分为多章 + +### 操作区 +![Functions][example] +- 按钮① 选中时表示当前书籍使用的目录正则 如果Legado的自动识别的目录情况不太理想,用户可以手动点击各个规则前面的按钮,临时对本书启用该规则,该按钮仅**针对当前书籍生效**。 +- 按钮组② 左边的开关被点亮时,表示该规则将会在自动识别目录时尝试进行匹配,该规则针对**所有TXT书籍生效**。开启后会对所有的TXT格式的书籍启用当前规则扫描符合条件的标题格式;中间的按钮表示编辑当前规则,当识别出的目录与你所期望的不一致时,可以修改当前规则以适应你所导入的书籍;右边的按钮表示删除当前规则,当用户不需要当前规则时可直接删除。(默认的规则删除后可点击 导入默认规则 按钮恢复) +- 按钮组③ 在当前界面进行操作后,需要点击确认按钮使得选择生效。 + +[example]:data:image/jpg;base64,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I1JsdAAAAAAAAAAAAAAAAEJsZCBVbnRGI1JsdAAAAAAAAAAAAAAAAFJzbHRVbnRGI1B4bEBSAAAAAAAAAAAACnZlY3RvckRhdGFib29sAQAAAABQZ1BzZW51bQAAAABQZ1BzAAAAAFBnUEMAAAAATGVmdFVudEYjUmx0AAAAAAAAAAAAAAAAVG9wIFVudEYjUmx0AAAAAAAAAAAAAAAAU2NsIFVudEYjUHJjQFkAAAAAAAAAAAAQY3JvcFdoZW5QcmludGluZ2Jvb2wAAAAADmNyb3BSZWN0Qm90dG9tbG9uZwAAAAAAAAAMY3JvcFJlY3RMZWZ0bG9uZwAAAAAAAAANY3JvcFJlY3RSaWdodGxvbmcAAAAAAAAAC2Nyb3BSZWN0VG9wbG9uZwAAAAAAOEJJTQPtAAAAAAAQAEgAAAABAAEASAAAAAEAAThCSU0EJgAAAAAADgAAAAAAAAAAAAA/gAAAOEJJTQQNAAAAAAAEAAAAHjhCSU0EGQAAAAAABAAAAB44QklNA/MAAAAAAAkAAAAAAAAAAAEAOEJJTScQAAAAAAAKAAEAAAAAAAAAAThCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklNBAAAAAAAAAIAADhCSU0EAgAAAAAADgAAAAAAAAAAAAAAAAAAOEJJTQQwAAAAAAAHAQEBAQEBAQA4QklNBC0AAAAAAAYAAQAAAAI4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQQeAAAAAAAEAAAAADhCSU0EGgAAAAADlwAAAAYAAAAAAAAAAAAAAlgAAAEsAAAAMQBTAGMAcgBlAGUAbgBzAGgAbwB0AF8AMgAwADIAMAAtADEAMQAtADIANAAtADEANgAtADMANAAtADQANQAtADUAMQA0AF8AaQBvAC4AbABlAGcAYQBkAG8ALgBhAHAAcAAuAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEsAAACWAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAABAAAAABAAAAAAAAbnVsbAAAAAIAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABSY3QxAAAABAAAAABUb3AgbG9uZwAAAAAAAAAATGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAACWAAAAABSZ2h0bG9uZwAAASwAAAAGc2xpY2VzVmxMcwAAAAFPYmpjAAAAAQAAAAAABXNsaWNlAAAAEgAAAAdzbGljZUlEbG9uZwAAAAAAAAAHZ3JvdXBJRGxvbmcAAAAAAAAABm9yaWdpbmVudW0AAAAMRVNsaWNlT3JpZ2luAAAADWF1dG9HZW5lcmF0ZWQAAAAAVHlwZWVudW0AAAAKRVNsaWNlVHlwZQAAAABJbWcgAAAABmJvdW5kc09iamMAAAABAAAAAAAAUmN0MQAAAAQAAAAAVG9wIGxvbmcAAAAAAAAAAExlZnRsb25nAAAAAAAAAABCdG9tbG9uZwAAAlgAAAAAUmdodGxvbmcAAAEsAAAAA3VybFRFWFQAAAABAAAAAAAAbnVsbFRFWFQAAAABAAAAAAAATXNnZVRFWFQAAAABAAAAAAAGYWx0VGFnVEVYVAAAAAEAAAAAAA5jZWxsVGV4dElzSFRNTGJvb2wBAAAACGNlbGxUZXh0VEVYVAAAAAEAAAAAAAlob3J6QWxpZ25lbnVtAAAAD0VTbGljZUhvcnpBbGlnbgAAAAdkZWZhdWx0AAAACXZlcnRBbGlnbmVudW0AAAAPRVNsaWNlVmVydEFsaWduAAAAB2RlZmF1bHQAAAALYmdDb2xvclR5cGVlbnVtAAAAEUVTbGljZUJHQ29sb3JUeXBlAAAAAE5vbmUAAAAJdG9wT3V0c2V0bG9uZwAAAAAAAAAKbGVmdE91dHNldGxvbmcAAAAAAAAADGJvdHRvbU91dHNldGxvbmcAAAAAAAAAC3JpZ2h0T3V0c2V0bG9uZwAAAAAAOEJJTQQoAAAAAAAMAAAAAj/wAAAAAAAAOEJJTQQRAAAAAAABAQA4QklNBBQAAAAAAAQAAAAKOEJJTQQMAAAAABT5AAAAAQAAAFAAAACgAAAA8AAAlgAAABTdABgAAf/Y/+0ADEFkb2JlX0NNAAH/7gAOQWRvYmUAZIAAAAAB/9sAhAAMCAgICQgMCQkMEQsKCxEVDwwMDxUYExMVExMYEQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQ0LCw0ODRAODhAUDg4OFBQODg4OFBEMDAwMDBERDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCACgAFADASIAAhEBAxEB/90ABAAF/8QBPwAAAQUBAQEBAQEAAAAAAAAAAwABAgQFBgcICQoLAQABBQEBAQEBAQAAAAAAAAABAAIDBAUGBwgJCgsQAAEEAQMCBAIFBwYIBQMMMwEAAhEDBCESMQVBUWETInGBMgYUkaGxQiMkFVLBYjM0coLRQwclklPw4fFjczUWorKDJkSTVGRFwqN0NhfSVeJl8rOEw9N14/NGJ5SkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9xEAAgIBAgQEAwQFBgcHBgU1AQACEQMhMRIEQVFhcSITBTKBkRShsUIjwVLR8DMkYuFygpJDUxVjczTxJQYWorKDByY1wtJEk1SjF2RFVTZ0ZeLys4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9ic3R1dnd4eXp7fH/9oADAMBAAIRAxEAPwB8fDtyKy+pzS8PbU2n3b3OeHOrDPb6Xu9Kz6Vv/fEVvS73VPt9SsBjPUgO3SPS+1/SZub/ADSZ2FlY73VOyMemxhIez7XS1wcNzHNe31vpt3PYnDcwNLRnY+0ggj7ZTBBBY7/DfuuW7LmsVmuYw/WcP4vOQ+H8zQvkuZJ7xxZeGX/NZO6RkMdQH2MZ9pnYXbtI2fS2sd7f0n02ez2KJ6Zf9mdlCys1s3yAXSfTO1232qQd1Brg4dQpDmja0/baZA8B+mUSzLc0tObjlrgdw+2UQQdX/wCG/O/PQHM49L5jD4+uC4/D+Y1rkuZ20/VZf+9aeqUq9jdD6jl7/sgpyfTj1PSvpftmdu/bb7d21H/5q9f/AO4v/gtX/pRTDmMB2y4/8eLVPJ81E1LBliexxzB/6LlSp0U2ZFzKa43WENEmBqY7rS/5q9f/AO4v/gtX/pRIfVb6wAgjFggyCLa5BH/XEDzGGtMuP/GiocrnsXgyEdfRL/vXLc1zXFrtCOdQfxapFhYaySDvaH6dpJEH/NWl/wA1ev8A/cUf9uVf+lE5+rP1gEOfjSGDSbazDR7oH6T6KR5jDX87j/xopHK57/mcm+nok//Q6qq7rbL2twmk432rL3e0ls/acjbLgxzPfb7Mj1bavSx/0tH6VX78r6zNsvFGJVZWLa245LgJqJf69lk3/mt9J30WKT+n4ddxrGffjvve6xtDbwyXWPdY/wBOsjd77XvRP2M7/ubm/wDb3/mCaOIACthW7azHDkkZcQjZJ+T1eo8XqRX5X1jabvQxK3BrKzQSQS6wn9YZt9ev2tY79Fu9P+as/wBJWk49cyDbTdVVXXva6p86EMNlm2z9JZZ6dr6cbf8Ao/5nKt/0SJ+yP+72Z/29/wCYJfsj/u9mf9vf+YJXLt+LGIYgQRk2N/IWn0XBuxur5PqnYWYzC2try8EX5Odk/pHvAc+yr+ba/wD65/hFuKriYFeLZZaLbbrLWsY59zt521l7mNbo3866xWkoChXmrmcnuZDIG9Ii6raPq/57jfWTI+sdGLa/otLbC2sHcGh9gdu/SGup7ve9lf8ANM9K3eg9HyfrDd9XW3dSbZXnm5oDhW31jjm2ttlz8bZs9ZtJv/wH0Gep6S3bLK6q3W2vbXXWC59jyGtaB9JznO+i1qg3LxX4/wBrbfW7GI3C8PaayJ27vVn0/pJyDk/VcHtx3/nK9f8Ad4kGI/Od03e8H7Zts2C1oBJBf9n9VjfQb7m+lv8A6P8A8XQg9Js6m/p1zupgi6X7NzQx2zYHe5rNrf502Mb7K/Z/pP56zQrey1jX1OD2PEte0gtIPdrm+1yibK7Md9lTmvYWvAc0hwkBzXe5v7rgkxP/0fQ8jBfa+3bY1tWRsFzSzc/2e39FZuGz2/vMf6Vn6ar3qpk/VrDyX3uffe37Tay6wMLBqw2Oa3dsc7b+l/rq5fmuqdaW1B9WPtN7y8NcNw3foqtp9Xaw7vp1f6On1LFUv+svSsey6u1zw7HtrofDQZfYXsYGe73e6pySnVOplJZWR9ZemY7rBaLA2v05ftbB9Us27f0m79G26uy72/oq1Kr6w4FttlTG2l9NjKXgNB91m+PoPf8ARbW72/zn+iZZYkp00DJzsHE2/a8mrH3yW+q9rJjnb6jm+KJTay6plrJ2vEiefDtuasD6yY9mRmsrqx35FhxLAz0iA5hNtP6ba59fq7Ppsr/0jPTt9Oiy1NmSBYZuWxxyZBGZIjRJIobf3mH1gxvq31up7LerUUWlgY1wuqc0FjvUYXVud+99NrXs3qPScfofS+ijplfWcWyz125JudZWW+oyyu9rW0et/NfoGN/nf+EW1Vf+z+mizJY5rWuMVy1zmMe93oVF27Y51VZYx2yx/wDI9T6aPk5Ypxhe1vqb3VMrZo0k3PZTXuLvoe633peqtx9n/oS+WaPCcVTljB244dOv805tOb0FmA7Ct6ni2tsFotf61Yn1i99ntfZZ/pfz3Wfy1DpD+i4OLZh4vUMbItvc54Fb6w5zjW2lrGV1ve53tqWnXntfguzCwhtYsLmCCf0ReyzZu9P6Xpfo9/p/2FmW9TZ1Tptd7aXU7M7GrLXlh19Wi0Oa6pz/AMyxiR4gLsfZ/asgMU5CIjMcRq+OJ/D23//S9KfjY9lrbrKmvtZGx5AJEHc3/Nd7mKexkk7WydSYEkqllNyze/0xbuIZ9lcx0VNP+F+1Mn3e7+c9Rr/0H9E/WFWvH1n9a/7Ocf0zdX9n3wIpl/r7/a5zn7PS/wDJ1pKdU11mSWNM8+0f3Jw1gMhrQQAAQBwOPyrJzB9aXtvZhOxq3uZWMd9oBDXhzXZD37d25j2Psrqb6f6P0Wf6X9HYyT1o3YjsdtTatRmMcRMyza6s+79Ft9X/AIRJTfEAQBAGgAVTK6f9oyGZLMi7GuYx1W6ks1Y5zbIcL6r/AM6v81Urv+dBYDV6AcaHTMT6xa/09zXbm/o3elv2Wekp4jfrJ9prfluo+zurr9StgEtsHpfaTu+lts/WPS2f8GgQDuuhOUDcd9u+/hJL+y8iZ/aWWT5+gR9xxU1vSLLqzXd1DJtrd9Jjxjuae/uY/ELVay/tRoIxPbkSILo2xPukvDmfRUc4Zhwox9xyN1W70S1ri3fX9q9E5H6Nv6H1du7/AKtDhHj9pX+/PtD/AMLx/wDetevpNtTBXV1DKrrb9FjBjtaPgxuJtUB0VldYH2q91VLhcKIpZWX1n1a97cfHpd/OMb+erOOM89NLbJZmFtoYbC1zgZf9m9RzN9T9rPS/7+g9Lb1RuBf+1HOdeS8s3msuDNjeXYzWV/zvqpcI8ftKvfn04R5Qxg/bwv8A/9P0mzJx67G1WPDXujQzpuOyve6Ntfqv9lXqfzr/AObUzZWOXtEENMuHJ+i3+sgXYfqOsAsLKsiBkVgA7to2e1591XqVj0rf5H816Nv6VVHfVrpL7ci57HOflXMyLfdHvrcbG/Ra12z3e9m76CSnSNlY5e0RqfcOJ2+P7yfeySN7ZBgjcND+6srJ+q/SMnf6jHzaGtsIcPcGGo1B0t2/o/s7FM/VzpJtN3puFhtZduDiIdW30mtbHtaxzd+7b/pHpKdFllb52Pa+OdrgY/zfipKj03o2H02fs2/UvPvIP856XqfRaz/uPUrySmL3sYNz3BjeJcQBPxcmuupx63W32NqrbG57yGtEna3U/vOdtaoZOMzJpdRaT6TiCQ3QmDuA3e5LIxWXY4xw51Qaa3McyCWmpzLav5wPa73Ve5JTNl9NlIvZY11JBcLARtgfSO7+Sh15WLlY1luLdXkVgPaX1OD2yAZbuYXe5NVhVV4bsNznW1vFge58bneqXvtmB/wrkLD6dV0/Dupqe+z1Nz3PsguJ2NqHA/cqYkp//9T0PIzbKn2lrWGvGDDa1zosd6n0fQb9H+RTu/pOR+r/AKL+cVSz6yY7LcmoYuRYcW5lDiwMdudY41tdW0Wept3N/PatR1Vb3tscxrn1zseWgubPOx30mKUnxSU5WZ9YcbDfc19Fz/s721vc0CPcHv3NO76P6Jyaz6xUV1WWvxcjbVd6GjQSSWvtbZz7a3en/wCC1LXk+KW4+KSnHZ9ZsR9or9G0A5P2Xedm0H3j1XEO9tfs/wCMfvVzpnUWdSxRksqsoG4tNdwAcCA135pd+9t/4xXNzvFNJPKSnG+sX1jZ0OlzzjuvcGNc0zsZLnek0OfDvo/4RQ6V9Zf2j0IdV+zip4ubjOre/bWXOsrx/VZfsd+h/Tf6L6f6Fbbmtex1bwHseC1zHCQQdC1zT9JRbRTXUKGVsZS0bRUGgMA/d9ONm1JkMsft8PB+sv8AnOL9HtwNejP9bpzs4VztbafTY7dJqL2EMe4V/T9L/CMZ/wAIg9K6oeqdPvyDUKSwvr2tf6gP6Ntu5riyl30bf9H/AOB+9aDQGgNaA0NEADQADwhRe1raHtaA1oY7QaDUHwSY3//V7/Koyn3ucxr3OOz7Pc2za2mP5z1Kdw9T3e/2st+0f0a30mKOTjdcsfaaMtlTDa11LIGlYBZZQ+z0Xfzv8/6n+D/mf+EVq3MxqrPTe4hw27iGuLW7jDPVsaNlW7+WnszcKouFmTUwscGvDrGt2udqxj9zva98e1JTRoxfrD6jDkZzCwZLnva1rTOPHso/mKv0m/2/S+h7/U3sSsxvrGbWOrzKgwXFzmEc0yxzK59J3vazez/1Z+ju2Z+BUHG3KprDHbX77Gth2p2O3O+n7H+1J+fgVl4syqWGoTaHWNG0T6c2e72fpP0f9dJTRrxPrE1lAdn12FljjeS0AvrmvY0Ftf0m7Lvzf8N/waWDjfWOuzGdm5dVrGF/2pgiXhwaKtjxj1fzTw9+zbX/AMatXQ6jUHghJJTjfWTC+sGVjWDo+SKT6YArDvTe5wdus2W7fa+yv2Mf6lfpoPR8Dr9H1cbjdRsfZnes14aLosbQLa32YxzGu91npNv/AML/AIT0fVWtndT6d0+t1udkMoaxu87j7ts7fbW2Xv8Ad9HaoY/WOm5WAOo49/q4pdsD2teXby4Utq9Hb6/qusexvp+nvSZjPJ7PDwDg4vn4f0v3eNWLTmt6Z6NjyzKLbA15dvLC4v8AQ/SP9Xd6THV/T9f/AIy9B6TjdQx+nXV9QsNtri9zNzzYWsLG+31HS/8AnfVf9P8A7b/ma7teVj2Y5yQ8NpAcXPfLA3ZIt9UW7HVeltd6nqfzaHRm4mbi224lotY0OY4iQQ4N3bXNeGvb7XNe3/g/0n0EmF//1vRrsKu17ybLGstj1qmkBj9vt98tc/3M/R2em+v1K0KzpGBY+17m2brrG22H1X/TYS6tzfd7Njnfzbf0SjlZ9lN72NdU309m2h4PqXb/APQEOH/FM9lv6b+c9OtDt6zbQ+7f0/JfVS5zQ+lhe47TtBNe2v22t3XV+lbez0v570bbPSSUku6F0m59lj6CH3PFtjmPeyXjf7/Y4f6WxKzofSLfU34zSbaxU90u3bQQ9u187mva9jH+p9PeoV9ZdY6G9Py4e3fU8sGwjaHt3P3eze/9GmZ1q0tcD07J9VgJIDfY4ifbVc8Vus9rd381/wAXvSU6TQ1rQ1ohrQAAOwGgTrNf1i8sccfpuXZYDDWvaK2uP53v/Sbdv8pinT1O261tQwMmqXurc+1oa0OZs3u3Au3VbHWOqu/m7vS9NiSlurdC6b1ep9eYx257Qz1a3FrhtO9jm/mOdW76O9ih0/6v4HTumDpuK6xtfqC82kg2G1rmXNtcdnp+11NXt9L09iB9ZOu5fR8W23Hwjkbaw8Wnd6bSXbP0nptPsp+nb+krQuj/AFjys/6ujqt9VeNZ6zaS924UlptrodlNl2/0a/Vd/hfp0/zqTOY5vY4jI+zxAVxfpf3XYow6KMT7I2XUw5p3HUh5c6zVmzb/ADjvofQQ8XAx+n4dtGPu2uDnkuMkuLNng381jfzU2Lm2X9NOY1gseG2FjGyA/wBMvYzbHrbfW9P831/+uoPSOpX9S6ddkXVMqc0vY303EtcNgs3De1u3b6npf9b/AOtpMD//1/ToPMcd/BKSqGVhW23ue1jHF+z0shziH0bfpekwD/rvs/nv5nI/QqF2B1Yvusxuo+k57nFjX1+ozaTuYHVvdtrdUz9Az0Nm+v8ASXb77P0aU6UlKT4rMZgdambepy1zDvrbU0e/aG+y5oa9lW4fmM9T6f8A1mB6X1c2teepO2syX3Fg3AGp7mObjc7NtTWu2b67UlOtJS1VXCxcnHBF2Q7I9oALp5DrH7vd/Iexn+tStJKVqkJ0A7aABAy6b76DVS/0Xkgi0EiAD29Mtf7lDPx778I0VkPs3VE73GsPDH12Wse+pr3M9ZjHt+gkptGZ15UbZ9OyedjufgVWxsXIZ004tjwy4tsaH1ucQzeXmr07CKrP0LXs2fQQOlYOVgdPupyrfVe4veCH2WBoLGt2tff+k+m19n9tJT//0PTkkkklKSSSSUpZz7+pX5+RjYttNFeM2o/pKXWucbA530m5GPt27f3VorLN1uH1XLtdi33V3tpNb6Wbwdge14Pu9vKbLp2vWvJmwC/coRMhD0CQjL1e5j/Rn6fk403o9c/7mY3/ALCv/wDe1Z+bn9exrA2t9F7BbXRZYMctAst9P0qmtOaXbnetX7/5pTGXmgztzj+l9SPQP0J3ejrcU2YOn5t7b8jpucbGiAWscyRO7a/07G7uEyVVoTfnJsYhPi/WY4mPhjw/+gNukdZvprvrzcbZaxr2ziPBhw3t/wC1vmoXXdUxr6Ksi6i6vK9WshlDq3N21W3bg92Tf/o/3EF2Zl+pY6mvNqqc0NqpGP7a4Ab7ItapZGRbm5WI5uLkVNxzdZa+2sMaAaLauS//AEj2o6aUTdjrLutEZky44QEOHJ+hhifklweqMf33/9kAOEJJTQQhAAAAAABdAAAAAQEAAAAPAEEAZABvAGIAZQAgAFAAaABvAHQAbwBzAGgAbwBwAAAAFwBBAGQAbwBiAGUAIABQAGgAbwB0AG8AcwBoAG8AcAAgAEMAQwAgADIAMAAxADkAAAABADhCSU0EBgAAAAAABwABAAAAAQEA/+EOxWh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjM2YTcwM2M3LWNiOWMtN2Q0Zi05MDZmLWZkYmEwNjEzYTc5ZSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpiMzIyZjg5NS1mNmJjLTZkNDAtYTY2MS1hYzNlZTY0Nzg5OWMiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iODU2NzdDMTcyNUEwMDVDMUJCODcwRDIxQTdGMTk2MTciIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IiIHhtcDpDcmVhdGVEYXRlPSIyMDIwLTExLTI0VDE2OjM4OjAyKzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMC0xMS0yNFQxNjo0NDoyMiswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMC0xMS0yNFQxNjo0NDoyMiswODowMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjZkNWJiOTY2LTU5MTYtZTU0Mi1hMGM5LWM3Zjg1YzcxZTY2NCIgc3RFdnQ6d2hlbj0iMjAyMC0xMS0yNFQxNjo0NDoyMiswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMzIyZjg5NS1mNmJjLTZkNDAtYTY2MS1hYzNlZTY0Nzg5OWMiIHN0RXZ0OndoZW49IjIwMjAtMTEtMjRUMTY6NDQ6MjIrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSLikaAiIHBob3Rvc2hvcDpMYXllclRleHQ9IuKRoCIvPiA8cmRmOmxpIHBob3Rvc2hvcDpMYXllck5hbWU9IuKRoSIgcGhvdG9zaG9wOkxheWVyVGV4dD0i4pGhIi8+IDxyZGY6bGkgcGhvdG9zaG9wOkxheWVyTmFtZT0i4pGiIiBwaG90b3Nob3A6TGF5ZXJUZXh0PSLikaIiLz4gPC9yZGY6QmFnPiA8L3Bob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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 diff --git a/app/src/main/assets/web/help/md/webDavBookHelp.md b/app/src/main/assets/web/help/md/webDavBookHelp.md new file mode 100755 index 000000000..45f74ca7e --- /dev/null +++ b/app/src/main/assets/web/help/md/webDavBookHelp.md @@ -0,0 +1,60 @@ +# WebDav 书籍简明使用教程 + +> 本帮助页会在第一次进入时弹出,后续则不再出现,如想查看,请点击右上角 “**⁝**” > 帮助 查看此页。 + +虽然阅读主要是用来看网络小说的工具,但为了方便书友,也提供了一些本地书籍阅读的简单支持(epub、txt) + +但阅读本地书籍的一个难题就是如何在多设备上同步阅读进度以及书籍,假如换了设备之后,原来设备上的本地书籍也要再次手动导入,不太方便。 + +阅读本身没有自己的服务器,没有类似多看、微信读书那种服务器存储的可能性,但是,阅读支持 WebDav 备份,那么我们也可以利用 WebDav 来同步书籍。 + +### 前提条件 +1. 配置好书籍存储位置(WebDav书籍下载存储到的位置):依次点击我的/其他设置/书籍存储位置,选择书籍保存位置即可。 + +2. 配置好 WebDav 备份(WebDav书籍的保存位置):我的/备份与恢复/WebDav设置。这里需要配置 WebDav 备份的服务器地址、账号、密码。详细的配置方案这里不赘述,请看这篇文章:[坚果云注册与配置 · 语雀 (yuque.com)](https://www.yuque.com/legado/wiki/fkx510) 或点击该页面右上角的帮助按钮,查看配置方法。 + +### 上传书籍到 WebDav + +配置好 WebDav 后,从主界面进入 WebDav 书籍页没有任何书籍显示,这是很正常的,因为我们WebDav的服务器上还没有任何书籍。 + +目前将书籍上传到 WebDav 的方式有三种: + +1. App 上传已导入的本地书籍。 + + 长按已导入的本地书籍进入书籍详情 > 右上角 “**⁝**” 找到 **上传 WebDav** ,点击,等待几秒后即可上传成功。 + +2. App 上传已缓存的网络书籍。 + + 主界面右上角点击更多设置 > 点击缓存/导出,在此页面右上角 “**⁝**” 找到 **导出到 WebDav** 并勾选。那么在书籍导出的时候便会自动上传一份到 WebDav 服务器中。 + +3. 使用坚果云客户端/自建WebDav服务客户端上传。 + + 对于大部分用户来说,App上传足够了,但有些用户书籍数量可能比较大,那么我们不建议您一本一本通过 App 上传,更好的方式是使用您所使用的 WebDav 服务的客户端批量上传。 + + 假设我们使用的坚果云的 WebDav 服务,进入 [坚果云官网](https://www.jianguoyun.com/d/home#/) ,下载对应平台的客户端安装运行,找到文件夹目录 legado/books ,这里就是存放书籍的位置,您可以批量将书籍上传到该文件夹下。 + +**不管是使用上述的任何一种方式上传的书籍,为了确保上传无误,请您最好在上传书籍后进入 WebDav 书籍页 检查是否能看到已经上传的书籍。** + +### 下载 WebDav 书籍到本地 + +与上传方式的多种多样不同,下载书籍到本地的方式比较朴素。 + +在 **WebDav 书籍页** 浏览已经上传的书籍,找到自己要下载的书籍,点击 **加入书架** 按钮,软件则会自动下载该书籍到本地并加入到书架中。 + +### 注意事项 + +- 如果使用的是坚果云的 WebDav 服务,免费流量限额对于同步App设置等以及 **少量的书籍** 足够了。但是如果是频繁需要上传/下载书籍的用户流量可能就不太够用了,请注意个人的用量,避免出现超出限额影响 App 设置等的同步。 + +### 常见问题 + +- 进入 **WebDav书籍页** 提示 "获取WebDav书籍出错 webDav 没有配置"。 + + > 这是因为没有配置 WebDav 同步服务,按照上文 前提条件 中提到的配置 Webdav 同步的方法配置好就行了。 + +- A 设备上传的本地书籍能否在 B 设备上看到,是否能够自动加到书架? + + > 如果 A 设备和 B 设备配置了相同的 WebDav 服务,那么 B 在 **WebDav 书籍页** 就能看到 A 上传的书籍。但是无法直接在书架上看到该书籍,这个可能后续会想方案来做,目前必须自己在 **WebDav 书籍页** 找到该书籍手动点击 **加入书架** 导入才行。 + +- 本地书籍的阅读进度/书签等是否同步? + + > 可以同步。 \ No newline at end of file diff --git a/app/src/main/assets/web/help/md/webDavHelp.md b/app/src/main/assets/web/help/md/webDavHelp.md new file mode 100755 index 000000000..62f7e9b0d --- /dev/null +++ b/app/src/main/assets/web/help/md/webDavHelp.md @@ -0,0 +1,19 @@ +# WebDav备份教程 + +### 阅读支持云备份,采用WebDav协议,所有支持WebDav的云盘都可以,建议采用坚果云,每月免费1G流量,用来备份阅读足够了,下面就采用坚果云来讲解配置步骤. + +1. 打开坚果云网站 https://www.jianguoyun.com/d/home#/ +2. 如果没有注册过坚果云先注册一下 +3. 登录坚果云 +4. 右上角用户名点开点账户信息 +5. 点击安全选项 +6. 在第三方管理里添加应用 +7. 将应用示例里的服务器地址,用户名,和密码填到阅读的WebDav设置里 +8. 阅读的WebDav配置在我的-备份与恢复,创建子文件夹选项保持默认即可 +9. 设置完成后手动执行一下备份,看看是否成功 +10. 恢复时选择想要恢复的备份文件 + +### 自动备份说明 + +* 设置好备份之后每次退出App会自动进行备份 +* WebDav同一天的备份会覆盖,不同日期的备份不会覆盖 diff --git a/app/src/main/assets/web/help/md/xpathHelp.md b/app/src/main/assets/web/help/md/xpathHelp.md new file mode 100755 index 000000000..d127e1950 --- /dev/null +++ b/app/src/main/assets/web/help/md/xpathHelp.md @@ -0,0 +1,265 @@ +# xpath 路径表达式详解 + +_注:本文所有代码均通过 Chrome(版本 123.0.6312.86) 验证_ + +> XPath 规范中定义了 13 种不同的轴(axes)。 +> 轴表示与元素的关系,并用于定位元素树上相对于该元素的元素。 + +- `namespace`(不支持) +- `attribute` 元素的属性。它可以缩写为 `@` +- `self` 表示元素本身。它可以缩写为 `.` +- `parent` 当前元素的父元素。它可以缩写为 `..` +- `child` 当前元素的子元素。 +- `ancestor` 当前元素的所有直属祖先。 +- `ancestor-or-self` 当前元素及其所有直属祖先。 +- `descendant` 当前元素的所有递归子元素。 +- `descendant-or-self` 当前元素及其所有递归子元素。 +- `following` 当前元素之后出现的所有元素。无视元素层级,但不含直属后代。 +- `following-sibling` 当前元素之后出现的所有同级元素。 +- `preceding` 当前元素之前出现的所有元素。无视元素层级,但不含直属祖先。 +- `preceding-sibling` 当前元素之前出现的所有同级元素。 + +```js +// 轴的用法-> 轴名::表达式 +// 例: +> $x('//body/ancestor-or-self::*') +< [body, html] +``` + +#### 一、xpath 表达式的基本格式 + +> xpath 通过"路径表达式"(Path Expression)来选取元素。 +> 在形式上,"路径表达式"与传统的文件系统非常类似。 + +```txt +# "/"斜杠作为路径内部的分割符。 +# 同一个元素有绝对路径和相对路径两种写法。 +# 绝对路径必须用"/"起首,后面紧跟根元素,比如/step/step/...。 +# 相对路径则是除了绝对路径以外的其他写法,比如 step/step,也就是不使用"/"起首。 +# "."表示当前元素。 +# ".."表示当前元素的父元素 +``` + +### 二、选取元素的基本规则 + +```txt +- "/":表示选取根元素 +- "//":表示选取任意位置的某个元素 +- nodename:表示选指定名称的元素 +- "@": 表示选取某个属性 +``` + +### 三、选取元素的实例 + +```html + + + + + 标题 + + + +
    + Harry Potter +

    29.39

    +

    usd

    +
    +
    + Cpp高级编程 +

    39.95

    +

    rmb

    +
    +
    +
    +
    +
    +
    +
    +
    + + +``` + +```js +// 例1 +> $x('/') // 选取根元素,返回包含被选中元素的数组。 +< [document] +// 例2 +> $x('/html') // 选取根元素下的所有 html 子元素,这是绝对路径写法。 +< [html] +// 例3 +> $x('html/head/meta') // 选取 head 元素下的所有 meta 元素,这是相对路径写法。 +< [meta, meta] // , +// 例4 +> $x('//p') // 选取所有 p 元素,不管它们在哪里 +< [p, p, p, p] //

    29.39

    ,

    usd

    ,

    39.95

    ,

    rmb

    +// 例5 +> $x('html/body//a') // 选取 body 元素下的所有 a 元素 +< [a, a, a] // , , +// 例6 +> $x('//@lang') // 选取所有名为 lang 的属性。 +< [lang, lang] // lang="eng", lang="cn" +> $x('html/head/meta/@content') // 选取 head 元素下所有 meta 元素的 content 属性。 +< [content] // content="作者" +// 例7 +> $x('//meta/..') // 选取所有 meta 元素的父元素。(相同的结果只会返回一个) +< [head] // ... +``` + +### 四、xpath 的谓语条件(Predicate) + +> 所谓"谓语条件",就是对路径表达式的附加条件。 +> 所有的附加条件,都写在方括号 `[]` 中,用于对元素进一步的筛选。 +> 方括号内的表达式结果为 true 的元素才会被选取。 + +```js +// 例8 +> $x('html/head/meta[1]') // 选取 head 元素下的第一个 meta 元素 +< [meta] // +> $x('//p[1]') // 选取所有元素下的第一个 p 元素 +< [p, p] //

    29.39

    ,

    39.95

    +// 例9 +> $x('html/head/meta[last()]') // 选取 head 元素下的最后一个 meta 元素 +< [meta] // +// 例10 +> $x('html/head/meta[last()-1]') // 选取 head 元素下的倒数第二个 meta 元素 +< [meta] // +// 例11 +> $x('html/head/meta[position()>1]') // 选取 head 元素下的除了第一个元素外的所有 meta 元素 +< [meta] // +// 例12 +> $x('//title[@lang]') // 选取所有具有lang属性的title元素。 +< [title, title] // Harry Potter, Cpp高级编程 +// 例13 +> $x('//title[@lang="eng"]') // 选取所有lang属性的值等于"eng"的title元素。 +< [title] // Harry Potter +// 例14 +> $x('/html/body/div[dl]') // 选择 body 的 div 子元素,且被选中 的 div 元素必须带有 dl 子元素。 +< [div] //
    ...
    +// 例15 +> $x('/html/body/div[p>35.00]') // 选取 body 的 div 子元素,且被选中 div 元素的 p 子元素的值必须大于 35.00。 +< [div] //
    Cpp高级编程

    39.95

    rmb

    +> $x('/html/body/div[p="rmb"]') // 选取 body 的 div 子元素,且被选中 div 元素的 p 子元素的值必须等于 "rmb"。 +< [div] //
    Cpp高级编程

    39.95

    rmb

    +// 例16 +> $x('/html/body/div[p="rmb"]/title') // 在例14结果集中,选择title子元素。 +< [title] // Cpp高级编程 +// 例17 +> $x('/html/body/div/p[.>35.00]') // 选择值大于 35.00 的 "/html/body/div" 的 p 子元素。 +< [p] //

    39.95

    +``` + +### 五、通配符 + +- `\*` 表示匹配任何元素。 +- `@\*` 表示匹配任何属性名。 + +```js +// 例18 +> $x('//*') // 选取所有元素,结果以递归顺序返回 +< [html, head, meta, title, meta, body, div, title, p, p, div, title, p, p, div, dl, dd, a, dd, a, dd, a] +// 例19 +> $x('/*/*') // 选取所有第二层的元素 +< [head, body] // ..., ... +// 例20 +> $x('//dl[@id="list"]/*') // 选取 id="list" 的 dl 元素的所有子元素。 +< [dd, dd, dd] //
    ,
    ,
    +// 例21 +> $x('//title[@*]') // 选取所有带有属性的 title 元素。 +< [title, title] // Harry Potter, Cpp高级编程 +``` + +### 六、选择多个路径 + +- 用 `|` 合并多个表达式的选取结果。 + +```js +// 例22 +> $x('//title | //a') // 选取所有 title 和 a 元素。 +< [title, title, title, a, a, a] + +``` + +### 七、xpath 的函数 + +> xpath 函数的参数可以是静态字符串或表达式,且函数可以嵌套调用。 +> xpath 的索引均从1开始,而不是从0开始。 + +```js +// boolean(expression) 将表达式选取的结果转换为布尔值。 +> $x('boolean(//title)') +< true +// number([object]) 将表达式选取的结果转换为数字。(HTML元素内容默认均为字符串) +> $x('number(//p[1])') +< 29.39 +// round(decimal) 将数字参数转换为整数并四舍五入。 +> $x('round(//p[1])') +< 29 +// ceiling(number) 将数字参数转换为整数并向上取整。ceiling(5.2)=6 +> $x('ceiling(//p[1])') // 仅使用匹配表达式的第一个元素 +< 30 +// floor(number) 将数字参数转换为整数并向下取整。floor(5.8)=5 +> $x('floor(//p[1])') +< 29 +// concat( string1, string2 [,stringn]* ) 字符串拼接,参数为静态字符串或表达式 +> $x('concat("cost:", //p[1], //p[2])') // 仅使用匹配表达式的第一个元素 +< 'cost:29.39usd' +// contains(haystack, needle) 判断 haystack 是否包含 needle,返回 boolean +> $x('contains(//p[1], "29.39")') // 仅使用匹配表达式的第一个元素 +< true +> $x('//title[contains(., "Harry")]') // 选取内容中包含 "Harry" 的 title 元素。 +< [title] // Harry Potter +// count( node-set ) 统计表达式选取的元素个数。 +> $x('count(//p)') +< 4 +// id(expression) 根据 id 属性选取元素,若参数为表达式,将获取表达式结果作为id查询。 +> $x('id(//dl/@id)') // 等效于 $x('id("list")') +< [dl#list] //
    ...
    +// last() 返回当前路径表达式匹配的同级元素集合的成员数量。 +> $x('//p[last()]') +< [p, p] //

    usd

    ,

    rmb

    +// name([node-set]) 返回表达式选取集合的首个成员带命名空间的元素名,HTML中与local-name([node-set])等价。 +// local-name([node-set]) 返回表达式选取集合的首个成员本地元素名。 +> $x('local-name(//*[@id])') // +< 'dl' +// namespace-uri([node-set]) 获取选定节点集中第一个节点的命名空间URI。 +> $x('namespace-uri(//div)') +< 'http://www.w3.org/1999/xhtml' // HTML通常都返回这个固定值 +// normalize-space([string]) 去文本内容中的前后空白以及将内部连续的空白替换为单个空格 +> $x('normalize-space(" test string ")') +< 'test string' +// not(expression) 返回表达式的布尔反值。 +> $x('//title[not(@lang)]') +< [title] // 标题 +// position() 返回选定元素处于路径表达式匹配的同级元素集合中的位置。 +> $x('//meta[position()=2]') +< [meta] // +// starts-with(haystack, needle) 检查某个字符串 haystack 是否以另一个字符串 needle 开始。 +> $x('//title[starts-with(., "Cpp")]') +< [title] // Cpp高级编程</title] +// string([object]) 将给定参数转换为字符串 +> $x('string(//p)') +< '29.39' +// string-length([string]) 返回给定字符串的字符数量 +> $x('string-length(string(//p))') +< 5 +// substring(string, start[, length]) 截取字符串 +> $x('substring(string(//p), 1, 3)') +< '29.' +// substring-after(haystack, needle) 返回字符串 haystack 中第一个 needle 之后的字符串。 +> $x('substring-after(string(//p), ".")') +< '39' +// substring-before(haystack, needle) 返回字符串 haystack 中第一个 needle 之前的字符串。 +> $x('substring-before(string(//p), ".")') +< '29' +// sum([node-set]) 对给定集合的数字求和。若给定集合中存在非数字,则返回 NaN +> $x('sum(//p[1])') +< 69.34 +// translate(string, "abc", "XYZ") 依次替换 string 中出现的 a、b、c 为对应位置的 X、Y、Z。 +// 若第三个参数中的字符少于第二个参数,那么在第一个参数中相应的字符将被删除。 +> $x('translate("aabbcc112233", "ac2", "V8")') +< 'VVbb881133' +// true() 表示函数中的 true 布尔值 +// false() 表示函数中的 false 布尔值 +``` diff --git a/app/src/main/assets/web/images/bg.jpg b/app/src/main/assets/web/images/bg.jpg new file mode 100755 index 0000000000000000000000000000000000000000..f690acc0c2b93a80dfe96fa4957ba3106c045209 GIT binary patch literal 50755 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 q!d@2K~B|<(< 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/index.html b/app/src/main/assets/web/index.html new file mode 100755 index 000000000..b5429c2f7 --- /dev/null +++ b/app/src/main/assets/web/index.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<!-- + Forty by HTML5 UP + html5up.net | @ajlkn + Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) +--> +<html> + +<head> + <title>Legado web 导航 + + + + + + + + +
    + + + + + + + + + +
    + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/css/wifi_send.css b/app/src/main/assets/web/uploadBook/css/wifi_send.css new file mode 100755 index 000000000..1e6919e92 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/css/wifi_send.css @@ -0,0 +1,356 @@ +@charset "utf-8"; +/*reset*/ +html{-webkit-text-size-adjust:100%} +body,p,blockquote,ul,ol,li,h1,h2,h3,h4,h5,h6,dl,dd,input,textarea,button{margin:0; padding:0;} +body {background:#f9f9f6;font-size:14px;color:#333;font-family:"微软雅黑",Arial,sans-serif,Tahoma,Geneva;line-height:150%;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-touch-callout:none} +h1, h2, h3, h4, h5,em,i { font-weight:normal} +ul, ol { list-style:none } +em,i{ font-style: normal; } +img{border: 0;vertical-align: middle} + +/*common*/ +.pc .red{ + color: #ea5449; +} +.pc .black{ + color: #202020; +} +.pc .gray{ + color: #999; +} +.pc .f_20{ + font-size: 20px; +} +.pc .f_18{ + font-size: 18px; +} +.pc .f_36{ + font-size: 36px; +} +.pc .f_14{ + font-size: 14px; +} +.pc .ml_25{ + margin-left: 25px; +} +.pc body{ + background: url(../img/background.png) repeat; +} +.pc .inline-block{ + display: inline-block; +} +.pc .inline{ + display: inline; +} +/*private*/ +.pc .title_wrap{ + border-bottom: 1px solid #fff; + outline: 1px solid #f0efed; + width: 100%; +} +.pc .main_wrap{ + width: 900px; + margin: 0 auto; +} +.pc .s_title{ + width: 900px; + margin: 0 auto; + padding: 16px 17px; + box-sizing: border-box; +} +.pc .s_title .up{ + vertical-align: 2px; +} +.pc .s_logo{ + display: inline-block; + height: 40px; + width: 30px; + vertical-align: -3px; +} +.pc .top_cont{ + position: relative; + padding: 22px 0 22px 20px; + overflow: hidden; +} +.pc .top_cont .status{ + font-size: 12px; +} +.pc .top_cont .type{ + font-size: 12px; + margin-top: 8px; +} +.pc .top_cont .select_btn{ + position: absolute; + right: 0; + display: inline-block; + line-height: 50px; + height: 50px; + width: 180px; + text-align: center; + background-color: #ea5449; + color: #fff; + border-radius: 3px; +} +.pc .select_btn input { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + height: 50px; + opacity: 0; + cursor: pointer; + filter:alpha(opacity=0); +} +.pc .s_table table{ + width: 100%; + background-color: #fff; + border-collapse:collapse; + table-layout: fixed; +} +.pc .s_table table tr td span{ + display: inline-block; + +} +/*thead*/ +.pc .s_table thead tr{ + height: 40px; + font-size: 12px; +} +.pc .s_table thead tr td:first-child + td{ + width: 40%; + text-align: left; + padding-left: 15px; +} +.pc .s_table thead tr td{ + text-align: center; + border-bottom: 1px solid #f5f5f5; + border-right: 1px solid #f5f5f5; +} +.pc .s_table thead tr td:first-child + td + td + td{ + border-right: 0; +} +/*tbody*/ +.pc .s_table tbody tr:nth-child(odd){ + background-color: #fbfaf8; +} +.pc .s_table tbody tr td{ + text-align: center; + line-height: 20px; + padding: 11px 0; + font-size: 12px; + border-bottom: 1px solid #f5f5f5; + border-right: 1px solid #f5f5f5; +} +.pc .s_table tbody tr td:first-child{ + padding: 0 15px; +} +.pc .s_table tbody tr td:first-child + td + td{ + padding: 0 15px; +} +.pc .s_table tbody tr td:first-child + td + td + td{ + padding: 0 15px; + border-right: 0; +} +.pc .s_table tbody tr td:first-child + td{ + text-align: left; + padding-left: 15px; +} +.pc .last_row{ + color: #999; + border-bottom: 0; + column-span: 4; + padding: 12px; +} +.pc .op_right{ + height: 16px; + width: 16px; + display: inline-block; + background: url("../img/right.png") center no-repeat; + background-size: 16px auto; + vertical-align: -2px; +} +.pc .op_wrong{ + height: 12px; + width: 12px; + display: inline-block; + background: url("../img/wrong.png") center no-repeat; + background-size: 12px auto; +} +.pc .main_wrap .active{ + -webkit-box-shadow: 0 0 15px rgba(0,0,0,.1); + -moz-box-shadow: 0 0 15px rgba(0,0,0,.1); + box-shadow: 0 0 15px rgba(0,0,0,.1); +} +.pc .btn{ + display: inline-block; + line-height: 0; + float: right; +} +.pc .warning{ + display: inline-block; + line-height: 50px; + float: right; + color: red; +} +.pc .warning a{ + color: #0078a5; +} + +/*h5*/ +.h5 .red{ + color: #ea5449; +} +.h5 .black{ + color: #202020; +} +.h5 .gray{ + color: #999; +} +.h5 .f_18{ + font-size: 18px; +} +.h5 .f_15{ + font-size: 15px; +} +.h5 .f_14{ + font-size: 14px; +} +.h5 .ml_15{ + margin-left: 15px; +} +/*private*/ +.h5 .s_title{ + line-height: 60px; + border-bottom: 1px solid #f0efed; + background-color: #fdfdfd; + padding: 0 17px; +} +.h5 .s_title .up{ + vertical-align: 2px; +} +.h5 .s_logo{ + display: inline-block; + height: 24px; + width: 20px; + background: url("../img/logo.png") center no-repeat; + background-size: 18px auto; + vertical-align: -3px; +} +.h5 .s_table{ + margin-bottom: 75px; +} +.h5 .s_table table{ + width: 100%; + border-collapse:collapse; + table-layout: fixed; +} +.h5 .s_table thead tr{ + height: 56px; + font-size: 11px; +} +.h5 .s_table thead tr td:nth-child(2){ + text-align: left; + width: 40%; +} +.h5 .s_table thead tr td{ + text-align: center; + border-bottom: 1px solid #f0efed; +} +.h5 .s_table tbody tr td{ + text-align: center; + line-height: 20px; + padding: 20px 0; + font-size: 12px; + border-bottom: 1px solid #f0efed; +} +.h5 .s_table tbody tr td:first-child{ + padding: 0 10px; +} +.h5 .s_table tbody tr td:nth-child(3){ + padding: 0 10px; +} +.h5 .s_table tbody tr td:last-child{ + padding: 0 10px; +} +.h5 .s_table tbody tr td:nth-child(2){ + text-align: left; + font-size: 14px; +} +.h5 .op_right{ + height: 16px; + width: 16px; + display: inline-block; + background: url("../img/right.png") center no-repeat; + background-size: 16px auto; + vertical-align: -2px; +} +.h5 .op_wrong{ + height: 12px; + width: 12px; + display: inline-block; + background: url("../img/wrong.png") center no-repeat; + background-size: 12px auto; +} +.h5 .bottom_c{ + position:fixed; + bottom:0; + left:0; + right:0; + text-align: center; +} +.h5 .bottom_c .type{ + line-height: 25px; + color: #999; + font-size: 10px; + border-top: 1px solid #eeedea; + background: #f9f9f6; +} +.h5 .bottom_btn_wrap{ + height: 50px; + line-height:50px; + background-color:#e8554d; + font-size:17px; + color:#fff; +} +.h5 .bottom_btn_wrap input{ + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + width: 100%; + display:inline-block; + opacity: 0; +} +.emphasize{ + color: #ea5449; + font-weight: bold; +} +.none{ + display: none; +} +.mask{ width:100%; height:100%;position: fixed;top: 0; left: 0; right: 0; background: rgba(0,0,0,.7); z-index: 99; display: none;} +.c_tc{ background:url(../img/notice01.png) no-repeat left top; -webkit-background-size:171px 43px;position: fixed;; top: 50%;left: 50%; margin-top: -22px; margin-left: -80px; width: 171px; height: 43px; z-index: 100} +.t_tc{ background:url(../img/notice02.png) no-repeat left top; -webkit-background-size:216px 84px;position: fixed;; top: 10px;right: 20px; width: 216px; height: 84px; z-index: 100} +.safariWarn{background:url(../img/safari.png) no-repeat left top; -webkit-background-size:216px 84px;} +.close{ + position: fixed; + right: 15px; + top: 15px; + width: 30px; + height: 30px; + background: url("../img/close.png") no-repeat center; + -webkit-background-size: 15px 15px; +} + + + + + + + + + + diff --git a/app/src/main/assets/web/uploadBook/img/close.png b/app/src/main/assets/web/uploadBook/img/close.png new file mode 100755 index 0000000000000000000000000000000000000000..8dd309abf94e00aaf86f7cc861785ee57fabc532 GIT binary patch literal 1018 zcmaJ=PiWIn7!PjB7#o5@-3H1dRZy{c`O`WRmyI@Qw}2_7D_91?(&TLoTmG25+H4oc zb`ta;;=!FnK?dS3-t?fm2p)t9;z9AIc$w^?w;g=x*3QFb;N`vV@%?_^@B8!al#1j1 zy+geeMfGbFYMJb3;?;AE{I9a>Pslci^Hn_Q&ETdHAS!ElH3T%rm_=n|SPM7bqY;Yg zO4^kwuIkq$({mUj_F*E&CuoWq8H;?woI@DY(5&ss^!DpFG_Wn1zMj!p-B(cEo>&af zvMURbwkw_v04#EbAoOw5tBAIUSO5_}0LmISP@SIHVimK`*pm+fS877l5 zSuPERMTX08=_}kNz_XkHSpjlFisL0#l(-Do`)Cp^ux6yPTG)$4ESauj>`M?fn@y%E zFkUbVIZ+fL%R`<|5sy^3;9?_6xnZ)+prX(WY#-a63t~p2<~6WP6Q#Q;IDUuL4fo1K z3I-#?haAJkDYYGS{r|3x)A0^*8SU#mkT|R?_z0F!=rsb9G;Ssthw>#QKnC`Lis#L@ zt5~Xg*bD2P4-^HQ*A3HlWBWo2pzD(6hS+dTq^UAZC>Yzeq%1Gw#No8UWfXv~rg01GE(KIhD^A z3S1_aJy^>w);d5-3!7uvL$Q#EfpKU5>2&*wj8A;*%q=lGlaE|7?g5$W#@!z(c@vJN zW-HN`+RknGd#vjS%Wo#${;Jgm06U#{`S>VIEuVh=C@HKB`rYu{MzOSAFHN4ihm|=l-crPZP&qRphCq&Gee~BB5RUI=-?xbx&U280A-1Ki?R5_mS!Ap}GgC WpMOrSJam`7#jjDz7uEIL^zvV}>p~m= literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/uploadBook/img/logo.png b/app/src/main/assets/web/uploadBook/img/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..91ce916d41bf40824a71ad19971d10eca1e867cf GIT binary patch literal 2621 zcmb7GXHXMd77Y+7fk-hx08voDK=2ckB25H>q4yr5gd(9z=xV4Es#K9)q&Ecw5dj4W zN{7&;N+Ly?bR;6e#@)X=JF|D@z4PXrbI-r~=EfKqAi*Gh5C8xGYil7)0027T-^0v6 zV{|(e0%)LRq-Ul{LyN@z@rB*cs;R23WlS3BUI2h4K^viF7C5~z+2dhzZ;|^C%H`Ou~nqy z{oi%7HFd-`c*S&$%2uKGFgkjv@f~#6tXR1=xHn|xPx^JkpswM52lTo}T=~K*4ieOo z^5BNUjd&)h@9}s6KJdchgO2H~u-N-M`X;E~Dq^la26OZH*rk|nj*!D~S4l%2zqLu- zIIFutg?<}CuaHKZsw2>Vl4#SxCVUHiESh8IkIg5Gmf2nfG#}ZAFKY6bpt9qq6QAv- zs`n@8K}BQDGh_5&+oV#XQ==L*woaNFj%sP79G~XDby30?aAkK#0+S{joUtl*HMPRY z$}5~LM{h3GZn}OF^0Q|=Z*pk)sL{i?(8Zf4E~D)Y55`hdq`1T4c%#1IAvrZEcHF+G zA+upB*4W|`LQmk@q{x7ulV*QcsO6iI-%&e|CtQOgCzoUR@yKG;T zQiG0FdTj$e&+FJUcSLi6zb==oq}GPP$H=)FMG;Ywu*RopOWWe^?eIbUQ9}cJWyjk+ zGxRqWGXmI5sY>2SM*IcT;s+5CBWT6c7LJp$ZK)m4BM@Sb6fq9lo*D z;3a2uk7q{WDD#u&f~Yx>_nDEJ^3U$7-CLTeO&J14N;r(LRluu-kox7kdrY~w91$7I z`ywRLfg^5l!udyN*@NHyw*X($0~2qBvU#lF25xu~*tc6dln%02uAW8bU;)!t#fwB_ z*uSl0ZEh893K=fq7-dT_e9+PRXLJ`I{U}!>x;VxZ2W-*#Oh+mRuW#YVPo$qL7B|*2 z#xy3dh!@TAmj&dby+>4iwd2$o+S;{1MPZv$S~gTnGGZ}=b>g?iD96l#|791n!1zO} zXPt%I!RL6{1i3I~l!%GSbAc`QcJ8IRW9HMK|`Mh*7I5G7U-m>V&je zc^T*Sh1>Eq5`v`+fb~ZQCTyyNb9jJPoK6^D*p-kOanQ3v1F6BIjrWX{1%^VYu2#hj zU;MJ)A#5bjHVv7~sNUPLV%I%rBw6{Wy#%U4CTti!g2B z7Wc@?ZC&Uu<6`y5FDt{4;PQ`$+g)eG9Ro8(^(pe)R)#3zYRkf*9^}LJW9@@gA zBq-E9W&a5tDf|9**FpFoV!(ih_cg z6u+mjfs$A$0&5*P(w>>{g|P%5p_)PrmE0EV*8*Mk<_TyDh7n?FhC2!qPvW)JB z{y-II@Tgd?-=o3TvBPRF^)wZMA9pFA2(>|x>}d}6T$VfB4G7}8ZAGcyU8HIG#7Q z0+N^xtqU3a1zXejfnrtT#`sCDmBqYaUM%pZ$O-=8)H&veHxlMZq4f60!!Go@bmT%y z-bw_lP41=nQ_P>L%>FT~)eZN#a1m$=ERSI33(%>UkteznPkSN^XWxp1EAuao)cbXtJ% bzwAa>fnL_^%+ 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 Q11ta|Ev_I|A!7Lbe+26nkpKVy literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/uploadBook/img/right.png b/app/src/main/assets/web/uploadBook/img/right.png new file mode 100755 index 0000000000000000000000000000000000000000..0862df4d028c81f38edd6062506f19ff3530960f GIT binary patch literal 1124 zcmaJ=TWHfz7!Hb}&cS@}LD}Rin>xWZNiVC7tI{>CE7(zLVFMMHCMR3AB_}3lYc>!; zhj;-W1VJ2v%;|%Q=)?;sYV_rCT*c1p!);sTx>CI&c)l4C=ulnq>OB z1kIS0)vcK}k%lq0XfT|cbxJA%6N&#rHEkAc;S~In?>~jDbe{p46tqx}DUpj)YMf98 zA2Febk(oxQyV%8K2g1neKn93J3S9$rEvhb~thJ`VNhJ8VZedZEU|a~%M8&PDGT#_! z3lgH z5*Na0`*}tNa}CzZRa(~uzkHv{f7v|TsYsd3FvH0S5& z!q3}2tgXHuyQ&1@oN`e(bA0Gt@9GD2C#gX0c=+X!Yp2$yGoj?36SWgy?Dnl&zNtrS z)!DYHk?Q2c(RKb)YsU92o1U|{Qi;Wee0%5Rf95AIe$5?veD>xGG%gGr{CHPL?~-eM z4X5vHoyvc^Qv3eivw`iCJASOJAHJNAe$VHZyek{&ThiSA$}3+wy7TF|ipltxtyB-M a0Atkd>dyU#Q#IF}cOQ>73l|#O_WS~3%7ID% literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/uploadBook/img/safari.png b/app/src/main/assets/web/uploadBook/img/safari.png new file mode 100755 index 0000000000000000000000000000000000000000..0e8a6f788e6507ad15a367cbc321324d94f55d83 GIT binary patch literal 10146 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 UlpHF8!y-UROkT7^_~YmQ2fQajE&u=k literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/uploadBook/img/wrong.png b/app/src/main/assets/web/uploadBook/img/wrong.png new file mode 100755 index 0000000000000000000000000000000000000000..593ab6945886d01abc7dc676cbcaf1c79c328fb8 GIT binary patch literal 1345 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%xak-5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0TEsYHf z4V;|J4NNT^VR~KilS^|`^GaZPQxJOHaOwpmhui|7%`T}$nPsUdZbkXI3Se(rW#V>= zGfwlMdQ)(_#RaEceV}9XL6M6T*)Sns3IZ|VNf*e0C;rqtV2UpSChhP!ogWw&7&m&l zIEGZ*%9-fv%@ioowtQ#P{O$v-E|*^{V>;N%wa)26d;qs%t}@rD5N>g`ZM_^>X4|+t z9b=d428rkWIVG>4r7Ie<>ck9}b0>VBw<(Lt?wDPDzxdp5J8{*?C#Cz37cUFZ`n)&J zU*(_X+Awbwq0aQxS9f`@3RzVz*XVae6?bkmwS$X1#xkr;ay(fj7O56N=#qGDB6jn7@hL>xP4$vBpM91;^1F7*>Z_9u$hl8aQS7>JXkjtm-t;xE zRBu~{+u}7BGkSEzx|v#XG)1}=9DcaqV632Zgn5GDGDb%Igd)qie$}_%mN9m-I&vfk z7}+kl{yI=YWEa!>>Dp5J+!hs`lUa + + + + WiFi 传输 + + + set 限制解除 + +
    +
    +
    +
    + 阅读 + | + WiFi传输 +
    +
    +
    +
    +
    + +

    可支持文件格式:

    +

    TXT、EPUB、UMD、PDF、MOBI、AZW3、AZW

    +
    +
    + + +
    + 选择文件 + +
    +
    +
    + +
    + + + + + + + + + + + + + + +
    文件名大小操作
    请将图书或字体拖拽至此即可上传
    +
    +
    +
    + + + +
    +
    +
    +
    + + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/js/common.js b/app/src/main/assets/web/uploadBook/js/common.js new file mode 100755 index 000000000..53d0360fb --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/common.js @@ -0,0 +1,173 @@ +/** + * 公共函数 + */ +//全局的配置文件 +var config = { + fileTypes: "txt|epub|umd|pdf|mobi|azw3|azw", //允许上传的文件格式 "txt|epub" // |doc|docx|wps|xls|xlsx|et|ppt|pptx|dps + //url : "http://"+location.host+"?action=addBook",//"http://localhost/t/post.php",// + url: "../addLocalBook", + fileLimitSize : 500 * 1024 *1024 + +}; + +//文件对应序号 +var fileMap = {}; + +/** + * HTML5 和 flash 公用,所有文件对象集合 + * @var array + */ +var filesUpload = []; // + +//初始化表格 +init(); + +function init(){ + //判断浏览器的高度预留空表格 + var tr_num = parseInt(Math.round(((window.innerHeight || document.documentElement.clientHeight) *.5)/43)); + var item = '' + + '' + + '' + + '' + + '' + + ''; + var i = 0; + var HTML = ''; + while(i < tr_num){ + HTML = HTML + item; + i ++; + } + $('#drag table tbody').prepend(HTML); +} + +//统计文件大小 +function countFileSize(fileSize) +{ + var KB = 1024; + var MB = 1024 * 1024; + if(KB >= fileSize){ + return fileSize+"B"; + }else if(MB >= fileSize){ + return (fileSize/KB).toFixed(2)+"KB"; + }else{ + return (fileSize/MB).toFixed(2)+"MB"; + } +} + +//如果文件太长进行截取 +function substr_string(name) +{ + var maxLen = 30; + var len = name.length; + if(len < maxLen )return name; + + var lastIndex = name.lastIndexOf("."); + var suffix = name.substr(lastIndex); + var pre = name.substr(0,lastIndex); + var preLen = pre.length; + var preStart = preLen - 20; + //前面10个 + 后面5个 + var fileName = pre.substr(0,20) + "...." + pre.substr( preStart > 4 ? -4 : -preStart , 4)+suffix; + return fileName +} + + +function checkFile(file) { + if (!file.name || !file.name.toLowerCase().match('('+config.fileTypes+')$')) { + return "格式不支持"; + } + + var len = filesUpload.length; + for(var i=0; i< len; i++){ + if(filesUpload[i].name == file.name) { + return "文件已存在"; + } + } + return null; +} + +/** + * 添加文件时,回调的函数 + * @param object file 文件对象 + * @param int type 0 是swf 上传的,1 是html5上传的 + */ +function fileQueuedPC(file, type) +{ + var size=0 ,fid=file.id, name=""; + type = type || 0; + + if(file != undefined ) + { + //计算文件大小 单位MB + size = countFileSize(file.size); + name = substr_string(file.name); + + //如果没有找到这个节点,先创建 + if ($('#drag tbody tr:last-child').prev().attr('data-status') != 'init' ){ + var HTML = '' + + '' + + '' + + '' + + '' + + ''; + + $("#drag tbody tr:last-child").before(HTML); + } + + var i = $('#drag [data-status=init]').eq(0).index('#drag [data-js=item]'); + + $('#drag [data-js=item]').eq(i).children().eq(0).find('span').html(i+1); + $('#drag [data-js=item]').eq(i).children().eq(1).find('span').html(name); + $('#drag [data-js=item]').eq(i).children().eq(2).find('span').html(size); + $('#drag [data-js=item]').eq(i).children().eq(3).find('span i').addClass('red').html('0%'); + $('#drag [data-js=item]').eq(i).attr('data-status', 'ed'); + + fileMap[file.name] = $('#drag [data-js=item]').eq(i).children().eq(3).find('span i'); + + } +} + +//上传时返回的状态 +function uploadProgress(file, bytesLoaded, bytesTotal) +{ + fileMap[file.name].html(parseInt((bytesLoaded/bytesTotal)*100)+"%"); +} + + +//上传成功 +function uploadSuccess(file, serverData, res) +{ + fileMap[file.name].removeClass('red').addClass('op_right').html(''); +} + +/** + * 查找在数组中的位置 + */ +function findObjectKey (object, fid){ + var len = object.length; + for(var i=0; i -1) + files.splice(filesUploadKey, 1); + + return files; +} \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/js/html5_fun.js b/app/src/main/assets/web/uploadBook/js/html5_fun.js new file mode 100755 index 000000000..19c5a53e9 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/html5_fun.js @@ -0,0 +1,257 @@ +/** + * 处理拖拽上传 + */ + var isDragOver = false;//拖拽触发点 + var fileNumber = -1; //上传文件编号 + var fileNumberPex = "zyFileUpload_"; //编号前缀 + var currUploadfile = {}; //当前上传的文件对象 + + var uploadQueue = [];//上传队列集合 + var isUploading = false;//是否正在上传 + + var XHR; + try{ + XHR = new XMLHttpRequest(); + }catch(e){} + +(function(isSupportFileUpload){ + + //不支持拖拽上传,或者 不支持FormData ,显示WiFi表示 + if(!isSupportFileUpload){ + $("#drag tbody tr:last-child td span").html('您的浏览器不支持拖拽上传'); + return; + //更换样式 + }else{ + $("#drag tbody tr:last-child td span").html('请将图书或字体拖拽至此即可上传'); + } + + addEvent(); + + /** + * 添加事件 + */ + function addEvent(){ + var click = $('#click')[0]; + var dropArea = $('#drag')[0]; + click.addEventListener('change', handleDrop, false); + dropArea.addEventListener('dragover', handleDragOver, false); + dropArea.addEventListener('dragleave', handleDragLeave, false); + dropArea.addEventListener('drop', handleDrop, false); + } + + /** + * 松开拖拽文件的处理,进行上传 + */ + function handleDrop(evt){ + + evt.stopPropagation(); + evt.preventDefault(); + + $('#drag').removeClass('active'); + + isDragOver = false; + + var file={}; + var errorMsgs = []; + + var len = 0; + if(typeof (this.files) == 'object'){ + len = this.files.length; + }else{ + len = evt.dataTransfer.files.length; + } + + + for(var i=0; i < len; i++){ + fileNumber ++ ; + + if(typeof(this.files) == 'object'){ + file = this.files[i]; + }else{ + file = evt.dataTransfer.files[i]; + } + + //检测文件 + msg = checkFile(file); + //文件可以通过 + if(!msg){ + file.id = fileNumberPex+fileNumber; + + //添加全局 + filesUpload.push(file); + //添加上传队列 + uploadQueue.push(file); + //在页面进行展示 + fileQueuedPC(file, 1); + }else{ + errorMsgs.push(msg) + } + } + + if(errorMsgs.length>0){ + //只选择做一个进行上传 + if(len==1){ + alert(errorMsgs[0]); + + }else{ + alert("你选择了"+len+"个文件,只能上传"+(len - errorMsgs.length)+"个文件。\n请选择可支持文件格式且文件名不能重复。"); + } + } + + //拿出第一个,进行上传 + if(!isUploading && uploadQueue.length>0) uploadFiles(uploadQueue.shift()); + + //清空input内容,防止两次上传文件一样,change事件不触发 + document.getElementById('click').value = ''; + + } + + function handleDragOver(evt){ + + evt.stopPropagation(); + evt.preventDefault(); + //防止多次DOM操作 + if (!isDragOver) { + $('#drag').addClass('active'); + isDragOver = true; + } + + + } + + function handleDragLeave(evt){ + + evt.stopPropagation(); + evt.preventDefault(); + isDragOver = false; + $('#drag').removeClass('active'); + } + + + + function uploadFiles(file){ + console.log(file); + //正在上传 + isUploading = true; + //设置上传的数据 +// var reader = new FileReader(); +// reader.readAsDataURL(file); +// reader.onload = function (e) { +// var data = e.target.result; + var fd = new FormData(); + fd.append("fileName", file.name); + fd.append("fileData", file); + //设置当前的上传对象 + currUploadfile = file; + if(XHR.readyState>0){ + XHR = new XMLHttpRequest(); + } + + XHR.upload.addEventListener("progress", progress, false); + XHR.upload.addEventListener("load", requestLoad, false); + XHR.upload.addEventListener("error", error, false); + XHR.upload.addEventListener("abort", abort, false); + XHR.upload.addEventListener("loadend", loadend, false); + XHR.upload.addEventListener("loadstart", loadstart, false); + XHR.open("POST", config.url); + // XHR.setRequestHeader("Content-Type","application/octet-stream"); + XHR.send(fd); + XHR.onreadystatechange = function() { + + //只要上传完成不管成功失败 + if (XHR.readyState == 4 ){ + + if(XHR.status == 200){ + uploadSuccess(currUploadfile, {}, XHR.status) + }else{ + uploadError() + } + + //进行下一个上传 + nextUpload() + } + }; +// }; + } + + //请求完成,无论失败或成功 + function loadend(evt){ + // console.log("loadend",+new Date(),evt); + } + //请求开始 + function loadstart(evt){ + // console.log("loadstart",evt); + } + + //在请求发送或接收数据期间,在服务器指定的时间间隔触发。 + function progress(evt){ + uploadProgress(currUploadfile, evt.loaded || evt.position , evt.total) + } + + //在请求被取消时触发,例如,在调用 abort() 方法时。 + function abort(evt){ + // console.log("abort",evt); + } + + //在请求失败时触发。 + function error(evt){ + //终止ajax请求 + XHR.abort(); + uploadError(); + nextUpload(); + } + + //在请求成功完成时触发。 + function requestLoad(evt){ + // console.log("requestLoad", +new Date(),evt); + } + + //进行下一个上传 + function nextUpload(){ + isUploading = false; + if(uploadQueue.length>0){ + uploadFiles(uploadQueue.shift()); + }else{ + //米有正在上传的了 + currUploadfile = {} + } + } + + //上传出错误了,比如断网, + function uploadError(){ + //移除全局变量中的,上传出错的 + removeFileFromFilesUpload(filesUpload, currUploadfile.id); + var file = currUploadfile; + fileMap[file.name].removeClass('red').addClass('op_wrong').html(''); + } + + + //对外部注册的函数 + var HTML5Funs = { + /** + * 取消上传 + * @param string fid 文件的Id + */ + cancelUpload : function(fid){ + + var filesUploadKey = -1; + var uploadQueueKey = -1; + + + //从全局中删除文件 + removeFileFromFilesUpload(filesUpload, fid) + + //如果是正在上传的,AJAX取消 + if(currUploadfile.id == fid){ + XHR.abort(); + }else{ + //从上传队列中移除 + removeFileFromFilesUpload(uploadQueue, fid) + } + } + }; + + window.HTML5Funs = HTML5Funs; + + +})("FormData" in window && "ondrop" in document.body); \ No newline at end of file diff --git a/app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js b/app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js new file mode 100755 index 000000000..7c2430802 --- /dev/null +++ b/app/src/main/assets/web/uploadBook/js/jquery-1.4.2.min.js @@ -0,0 +1,154 @@ +/*! + * jQuery JavaScript Library v1.4.2 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Sat Feb 13 22:33:48 2010 -0500 + */ +(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, +Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& +(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, +a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== +"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, +function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
    a"; +var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, +parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= +false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= +s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, +applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; +else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, +a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== +w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, +cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, +function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); +k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), +C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= +e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& +f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; +if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", +e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, +"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, +d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); +t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| +g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, +CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, +g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, +text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, +setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= +h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== +"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, +h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& +q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; +if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

    ";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); +(function(){var g=s.createElement("div");g.innerHTML="
    ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: +function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= +{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== +"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", +d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? +a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== +1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
    ","
    "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, +""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); +return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", +""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= +c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? +c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= +function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= +Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, +"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= +a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= +a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== +"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
    ").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, +serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), +function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, +global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& +e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? +"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== +false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= +false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", +c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| +d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); +g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== +1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== +"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; +if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== +"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| +c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; +this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= +this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, +e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
    "; +a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); +c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, +d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- +f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": +"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in +e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/app/src/main/assets/web/vue/assets/BookChapter-BsiFtdIw.css b/app/src/main/assets/web/vue/assets/BookChapter-BsiFtdIw.css new file mode 100755 index 000000000..f0e8bfb82 --- /dev/null +++ b/app/src/main/assets/web/vue/assets/BookChapter-BsiFtdIw.css @@ -0,0 +1 @@ +@charset "UTF-8";.title[data-v-d8efefe3]{margin-bottom:57px;font:24px/32px PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,Microsoft YaHei,sans-serif}p[data-v-d8efefe3]{display:block;word-wrap:break-word;letter-spacing:calc(var(--v3af0ffa9) * 1em);line-height:calc(1 + var(--v316cdf92));margin:calc(var(--v3ab99b0b) * 1em) 0}p[data-v-d8efefe3] img{height:1em}.full[data-v-d8efefe3]{display:block;width:100%}@font-face{font-family:FZZCYSK;src:local("☺"),url(./popfont-WaOB0hHG.ttf);font-style:normal;font-weight:400}@font-face{font-family:iconfont;src:url(./iconfont-PstzbNMW.woff) format("woff")}[data-v-dd7cfcb2] .iconfont,[data-v-dd7cfcb2] .moon-icon{font-family:iconfont;font-style:normal}.settings-wrapper[data-v-dd7cfcb2]{-webkit-user-select:none;user-select:none;margin:-13px;text-align:left;padding:40px 0 40px 24px;background:#ede7da url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX48dr48Nf58tv379X17NJtIBxUAAACFUlEQVQ4y1XRUZakMAgF0Af2AiDWApDZgHZqAV1nZv9rGh7Rj7Y8McUFEg1wvcMESMNVD/neU8Xcaz7nYYkYlYO6Ti82PBI4BvIEg1aj3wKwRvIMgZsUy5LdhCawPFh1sZs4SrlyN9fQKpv8s5dgZ2eLyqqJiu+WkCmUEybXkm3INS01WAiv0PapJ0CZc0SJQUzcWnZYbOOY20iFD8Bk+/j2A3wNxH7GdShFYS5ff237kXh9I9zSkQmIAhOsOSVfJ6DIXTMDaPnzkRJ92S1BQQmXl5LdirgRLLDdcYqcGPwe3QN4xCBiGNbrqq9wpW1XCecChwaQdVOsRDpPCpeoolPdxeXp3WNB9PHVzWBHlygy4NJCCrFHREv6bDt0VGwJZASkpONmm1UseGeFKAQexgaAkrfYWl3AGxWOLL2AIMBNbCXpktmS3k3vHeYjGCPBa43wJTurO3ZFVpQSJdAZGLoHTyk1upkjxMEaIxum3iIARcCa5kSkFAW5fi1mUlL9eyOsaanFmOMruwvEdE3ZYzsRSzo5ewRLXyVPPEvknt8ij4DvCg2O7xOgBCUprEzV4z1WekSpUgI8DT2mrnSOXKRfQavwuKA1F+tFnMKdJSUpMA7wQAifWRkMgjUKKZE4lBl6MCM4B1pq1P4uIjDE6Pq6rL0FnW1nIFmta5vrSvq/Ch4tpqG/ZNyyWa5jZPktq81eYv8Bt5s4iFITOp4AAAAASUVORK5CYII=) repeat}.settings-wrapper .settings-title[data-v-dd7cfcb2]{font-size:18px;line-height:22px;margin-bottom:28px;font-family:FZZCYSK;font-weight:400}.settings-wrapper .setting-list[data-v-dd7cfcb2]{max-height:calc(70vh - 50px);overflow:auto}.settings-wrapper .setting-list ul[data-v-dd7cfcb2]{list-style:none outside none;margin:0;padding:0}.settings-wrapper .setting-list ul li[data-v-dd7cfcb2]{list-style:none outside none}.settings-wrapper .setting-list ul li i[data-v-dd7cfcb2]{font:12px/16px PingFangSC-Regular,-apple-system,Simsun;display:inline-block;min-width:48px;margin-right:16px;vertical-align:middle;color:#666}.settings-wrapper .setting-list ul li .theme-item[data-v-dd7cfcb2]{line-height:32px;width:34px;height:34px;margin-right:16px;margin-top:5px;border-radius:100%;display:inline-block;cursor:pointer;text-align:center;vertical-align:middle}.settings-wrapper .setting-list ul li .theme-item .iconfont[data-v-dd7cfcb2]{display:none}.settings-wrapper .setting-list ul li .selected[data-v-dd7cfcb2]{color:#ed4259}.settings-wrapper .setting-list ul li .selected .iconfont[data-v-dd7cfcb2]{display:inline}.settings-wrapper .setting-list ul .font-list[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .infinite-loading[data-v-dd7cfcb2]{margin-top:28px}.settings-wrapper .setting-list ul .font-list .font-item[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .font-list .infinite-loading-item[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .infinite-loading .font-item[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .infinite-loading .infinite-loading-item[data-v-dd7cfcb2]{width:78px;height:34px;cursor:pointer;margin-right:16px;border-radius:2px;text-align:center;vertical-align:middle;display:inline-block;font:14px/34px PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,Microsoft YaHei,sans-serif}.settings-wrapper .setting-list ul .font-list .font-item-input[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .infinite-loading .font-item-input[data-v-dd7cfcb2]{width:168px;color:#000}.settings-wrapper .setting-list ul .font-list .selected[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .infinite-loading .selected[data-v-dd7cfcb2]{color:#ed4259;border:1px solid #ed4259}.settings-wrapper .setting-list ul .font-list .font-item[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .font-list .infinite-loading-item[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .infinite-loading .font-item[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .infinite-loading .infinite-loading-item[data-v-dd7cfcb2]:hover{border:1px solid #ed4259;color:#ed4259}.settings-wrapper .setting-list ul .font-size[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .read-width[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .letter-spacing[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .line-spacing[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .paragraph-spacing[data-v-dd7cfcb2]{margin-top:28px}.settings-wrapper .setting-list ul .font-size .resize[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .read-width .resize[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .letter-spacing .resize[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .line-spacing .resize[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .paragraph-spacing .resize[data-v-dd7cfcb2]{display:inline-block;width:274px;height:34px;vertical-align:middle;border-radius:2px}.settings-wrapper .setting-list ul .font-size .resize span[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .read-width .resize span[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .letter-spacing .resize span[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .line-spacing .resize span[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .paragraph-spacing .resize span[data-v-dd7cfcb2]{width:89px;height:34px;line-height:34px;display:inline-block;cursor:pointer;text-align:center;vertical-align:middle}.settings-wrapper .setting-list ul .font-size .resize span em[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .read-width .resize span em[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .letter-spacing .resize span em[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .line-spacing .resize span em[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .paragraph-spacing .resize span em[data-v-dd7cfcb2]{font-style:normal}.settings-wrapper .setting-list ul .font-size .resize .less[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .font-size .resize .more[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .read-width .resize .less[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .read-width .resize .more[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .letter-spacing .resize .less[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .letter-spacing .resize .more[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .line-spacing .resize .less[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .line-spacing .resize .more[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .paragraph-spacing .resize .less[data-v-dd7cfcb2]:hover,.settings-wrapper .setting-list ul .paragraph-spacing .resize .more[data-v-dd7cfcb2]:hover{color:#ed4259}.settings-wrapper .setting-list ul .font-size .resize .lang[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .read-width .resize .lang[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .letter-spacing .resize .lang[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .line-spacing .resize .lang[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .paragraph-spacing .resize .lang[data-v-dd7cfcb2]{color:#a6a6a6;font-weight:400;font-family:FZZCYSK}.settings-wrapper .setting-list ul .font-size .resize b[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .read-width .resize b[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .letter-spacing .resize b[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .line-spacing .resize b[data-v-dd7cfcb2],.settings-wrapper .setting-list ul .paragraph-spacing .resize b[data-v-dd7cfcb2]{display:inline-block;height:20px;vertical-align:middle}.night[data-v-dd7cfcb2] .theme-item,.night[data-v-dd7cfcb2] .selected{border:1px solid #666}.night[data-v-dd7cfcb2] .moon-icon{color:#ed4259}.night[data-v-dd7cfcb2] .font-list .font-item,.night[data-v-dd7cfcb2] .font-list .infinite-loading-item,.night .infinite-loading .font-item[data-v-dd7cfcb2],.night .infinite-loading .infinite-loading-item[data-v-dd7cfcb2],.night[data-v-dd7cfcb2] .resize{border:1px solid #666;background:#2d2d2d80}.night[data-v-dd7cfcb2] .resize b{border-right:1px solid #666}.day[data-v-dd7cfcb2] .theme-item{border:1px solid #e5e5e5}.day[data-v-dd7cfcb2] .selected{border:1px solid #ed4259}.day[data-v-dd7cfcb2] .moon-icon{display:inline;color:#fff3}.day[data-v-dd7cfcb2] .font-list .font-item,.day[data-v-dd7cfcb2] .font-list .infinite-loading-item,.day .infinite-loading .font-item[data-v-dd7cfcb2],.day .infinite-loading .infinite-loading-item[data-v-dd7cfcb2]{background:#ffffff80;border:1px solid rgba(0,0,0,.1)}.day[data-v-dd7cfcb2] .resize{border:1px solid #e5e5e5;background:#ffffff80}.day[data-v-dd7cfcb2] .resize b{border-right:1px solid #e5e5e5}@media screen and (max-width: 500px){.settings-wrapper i[data-v-dd7cfcb2]{display:flex!important;flex-wrap:wrap;padding-bottom:5px!important}}.selected[data-v-a892cd6d]{color:#eb4259}.wrapper[data-v-a892cd6d]{display:flex}.wrapper .cata-text[data-v-a892cd6d]{width:100%;margin-right:26px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.cata-wrapper[data-v-6cab38af]{margin:-16px;padding:18px 0 24px 25px}.cata-wrapper .title[data-v-6cab38af]{font-size:18px;font-weight:400;font-family:FZZCYSK;margin:0 0 20px;color:#ed4259;width:fit-content;border-bottom:1px solid #ed4259}.cata-wrapper[data-v-6cab38af] .data-wrapper .cata{height:40px;cursor:pointer;font:16px/40px PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,Microsoft YaHei,sans-serif}.cata-wrapper .night[data-v-6cab38af] .cata{border-bottom:1px solid #666}.cata-wrapper .day[data-v-6cab38af] .cata{border-bottom:1px solid #f2f2f2}[data-v-fff9fad7] .pop-setting{margin-left:68px;top:0}[data-v-fff9fad7] .pop-cata{margin-left:10px}.chapter-wrapper[data-v-fff9fad7]{padding:0 4%;overflow-x:hidden}.chapter-wrapper[data-v-fff9fad7] .no-point{pointer-events:none}.chapter-wrapper .tool-bar[data-v-fff9fad7]{position:fixed;top:0;left:50%;z-index:100}.chapter-wrapper .tool-bar .tools[data-v-fff9fad7]{display:flex;flex-direction:column}.chapter-wrapper .tool-bar .tools .tool-icon[data-v-fff9fad7]{font-size:18px;width:58px;height:48px;text-align:center;padding-top:12px;cursor:pointer;outline:none}.chapter-wrapper .tool-bar .tools .tool-icon .iconfont[data-v-fff9fad7]{font-family:iconfont;width:16px;height:16px;font-size:16px;margin:0 auto 6px}.chapter-wrapper .tool-bar .tools .tool-icon .icon-text[data-v-fff9fad7]{font-size:12px}.chapter-wrapper .read-bar[data-v-fff9fad7]{position:fixed;bottom:0;right:50%;z-index:100}.chapter-wrapper .read-bar .tools[data-v-fff9fad7]{display:flex;flex-direction:column}.chapter-wrapper .read-bar .tools .tool-icon[data-v-fff9fad7]{font-size:18px;width:42px;height:31px;padding-top:12px;text-align:center;align-items:center;cursor:pointer;outline:none;margin-top:-1px}.chapter-wrapper .read-bar .tools .tool-icon .iconfont[data-v-fff9fad7]{font-family:iconfont;width:16px;height:16px;font-size:16px;margin:0 auto 6px}.chapter-wrapper .chapter[data-v-fff9fad7]{font-family:Microsoft YaHei,PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,sans-serif;text-align:left;padding:0 65px;min-height:100vh;width:670px;margin:0 auto}.chapter-wrapper .chapter .content[data-v-fff9fad7]{font-size:18px;line-height:1.8;font-family:Microsoft YaHei,PingFangSC-Regular,HelveticaNeue-Light,Helvetica Neue Light,sans-serif}.chapter-wrapper .chapter .content .bottom-bar[data-v-fff9fad7],.chapter-wrapper .chapter .content .top-bar[data-v-fff9fad7]{height:64px}.day[data-v-fff9fad7] .popup{box-shadow:0 2px 4px #0000001f,0 0 6px #0000000a}.day[data-v-fff9fad7] .tool-icon{border:1px solid rgba(0,0,0,.1);margin-top:-1px;color:#000}.day[data-v-fff9fad7] .tool-icon .icon-text{color:#0006}.day[data-v-fff9fad7] .chapter{border:1px solid #d8d8d8;color:#262626}.night[data-v-fff9fad7] .popup{box-shadow:0 2px 4px #0000007a,0 0 6px #00000029}.night[data-v-fff9fad7] .tool-icon{border:1px solid #444;margin-top:-1px;color:#666}.night[data-v-fff9fad7] .tool-icon .icon-text{color:#666}.night[data-v-fff9fad7] .chapter{border:1px solid #444;color:#666}.night[data-v-fff9fad7] .popper__arrow{background:#666}@media screen and (max-width: 776px){.chapter-wrapper[data-v-fff9fad7]{padding:0}.chapter-wrapper .tool-bar[data-v-fff9fad7]{left:0;width:100vw;margin-left:0!important}.chapter-wrapper .tool-bar .tools[data-v-fff9fad7]{flex-direction:row;justify-content:space-between}.chapter-wrapper .tool-bar .tools .tool-icon[data-v-fff9fad7]{border:none}.chapter-wrapper .read-bar[data-v-fff9fad7]{right:0;width:100vw;margin-right:0!important}.chapter-wrapper .read-bar .tools[data-v-fff9fad7]{flex-direction:row;justify-content:space-between;padding:0 15px}.chapter-wrapper .read-bar .tools .tool-icon[data-v-fff9fad7]{border:none;width:auto}.chapter-wrapper .read-bar .tools .tool-icon .iconfont[data-v-fff9fad7]{display:inline-block}.chapter-wrapper .chapter[data-v-fff9fad7]{width:100vw!important;padding:0 20px;box-sizing:border-box}} diff --git a/app/src/main/assets/web/vue/assets/BookChapter-Cs3stH93.js b/app/src/main/assets/web/vue/assets/BookChapter-Cs3stH93.js new file mode 100755 index 000000000..6107c782d --- /dev/null +++ b/app/src/main/assets/web/vue/assets/BookChapter-Cs3stH93.js @@ -0,0 +1 @@ +import{d as ce,a4 as Je,U as Fe,a9 as Pe,o as g,e as p,F as se,h as t,z as K,R as le,u as a,aa as Z,C as l,M as D,ab as Me,N as be,y as Q,f as H,ac as Ge,w as z,ad as qe,ae as Ze,H as fe,af as Le,A as Ye,g as q,P as pe,a8 as Re,n as R,ag as je,a6 as Ke,ah as Xe,V as _e,Q as De,a7 as $e,ai as et,aj as tt,c as ot}from"./vendor-KSDcS24u.js";import{u as me,i as Qe,A as ne,_ as Ae,c as Ve}from"./index-Wr40-hHf.js";import{u as nt}from"./loading-C4J6hIxs.js";const st=(c,s,A,f)=>(c/=f/2,c<1?A/2*c*c+s:(c--,-A/2*(c*(c-2)-1)+s)),at=()=>{let c,s,A,f,n,w,m,y,h,F,P,x,S;function V(){let u=c.scrollTop||c.scrollY||c.pageYOffset;return u=typeof u>"u"?0:u,u}function U(u){const i=u.getBoundingClientRect().top,d=c.getBoundingClientRect?c.getBoundingClientRect().top:0;return i-d+A}function C(u){c.scrollTo?c.scrollTo(0,u):c.scrollTop=u}function I(u){F||(F=u),P=u-F,x=w(P,A,y,h),C(x),P({v3af0ffa9:y.spacing.letter,v316cdf92:y.spacing.line,v3ab99b0b:y.spacing.paragraph}));const f=me(),n=l(()=>f.config.readWidth),w=l(()=>f.config.fontSize),m=l(()=>f.readingBook.bookUrl),y=c,h=/]*src=['"]([^'"]*(?:['"][^>]+\})?)['"][^>]*>/g,F=i=>i.replace(h,(d,B)=>{if(Qe(B)){const W=ne.getProxyImageUrl(m.value,B,w.value*2);return d.replace(B,W)}return d}),P=i=>{const d=/]*src=['"]([^'"]*(?:['"][^>]+\})?)['"][^>]*>/,B=i.match(d)[1];return Qe(B)?ne.getProxyImageUrl(m.value,B,n.value):B},x=i=>{var B;const d=(B=i.target)==null?void 0:B.getAttribute("src");d!=null&&d.length>0&&(i.target.src=ne.getProxyImageUrl(m.value,d,n.value))},S=i=>{var d;((d=i.target)==null?void 0:d.tagName)==="IMG"&&x(i)},V=i=>i.replace(h," ").length,U=l(()=>{let i=-1;return Array.from(y.contents,d=>(i+=V(d)+1,i))}),C=D(),I=D();s({scrollToReadedLength:i=>{if(i===0)return;const d=U.value.findIndex(B=>B>=i);d!==-1&&Me(()=>{te(I.value[d],{duration:0})})}});let v=null;const u=A;return Fe(()=>{v=new IntersectionObserver(i=>{for(const{target:d,isIntersecting:B}of i)B&&u("readedLengthChange",y.chapterIndex,parseInt(d.dataset.chapterpos))},{rootMargin:`0px 0px -${window.innerHeight-24}px 0px`}),v.observe(C.value),I.value.forEach(i=>{v.observe(i)})}),Pe(()=>{v==null||v.disconnect(),v=null}),(i,d)=>(g(),p(se,null,[t("div",{class:"title","data-chapterpos":"0",ref_key:"titleRef",ref:C},K(c.title),513),(g(!0),p(se,null,le(c.contents,(B,W)=>(g(),p("div",{key:W,ref_for:!0,ref_key:"paragraphRef",ref:I,"data-chapterpos":a(U)[W]},[/^\s*]*src[^>]+>$/.test(String(B))?(g(),p("img",{key:0,class:"full",src:P(B),onErrorOnce:x,loading:"lazy"},null,40,rt)):(g(),p("p",{key:1,style:Z({fontFamily:c.fontFamily,fontSize:a(w)}),innerHTML:F(B),onErrorCapture:S},null,44,lt))],8,it))),128))],64))}}),At=Ae(ct,[["__scopeId","data-v-d8efefe3"]]),dt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXr5djn4dTp49bt59rT6LKxAAACnElEQVQozw3NUUwScRzA8d8R6MF8YMIx8uk47hDSJbj14IPzOGc7jPLvwTGg5uAYDbe2tt56cLtznvEnS6yDqCcEaWi91DvrbLJZz7b1aFtz1aO+2OZWvn+/+4CHeB6BMYaqBLfjPNRY6RFT2JJYby+uAk4WUTrtlmJ4hgPYb2q1XGDQjaK8pgJHvqNaAX+KyuIkDXpgQinb46nOulnn4b5laUHTxLfseeArAoNOeJlOIjdoal0n1FA7tKFv5roK+YaHOqP3P0XyKHPHY+MhTRe5uCZnKhtJKw2eSrSoBDPLtpZuNcFNJcFyiCMxOaaHIfXz1e8HQbWLySrBQ4x0x1qlhnHlnz2HQEC6TNb0gTHXa7IKhcaHqkE015hk9whA0YeWiLIXf7Fa2CZo3DjqjB4tTuF8jIcbfcEx5z/w4sXpQhXW+ju0cqh7icTFmRMaG+v6CIvTjcSpHcH8JEsF3EPh3fRthYdVLLgI2fWXm85/pGFE4l046s70L+yKCcirGFR+jbpy3kMmiCGHrSezVONsn1RBixncyk2PcVWk7DlgxHo8iZwDyq5uAUD854dZhdIFYzKoQig2haUKi1lVufz2RZUZPZ41n/hrOQB6h0Hhg8I367FNoEHgeM/KY7szSeQwD8q2WE3HM35ZLl0K1MJiOtHIkBclRQUwZnyOWcNsRQQgVLj1PSqkjF9DsoOSaSg3iinKzvfmgsNFFfpP/2T3GLGvL4fHEfwIX1sVvXcPqLztehWGcfn9nI2U9nTfCgJPe/jFPLZwgVEzimBgAm0VIyK2tt1cE/AzQdLK+SxLSQ4aDCZnnId94OG2S1XwvnTbNk/ZnhyRCQT+sZM6z9g6LXL1BOBe+zJySiFkHAINCtnQokbCJ/apCv0foqPiZVfhpywAAAAASUVORK5CYII=",ut="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAACVBMVEX28ef48+n69esoK7jYAAAB4UlEQVQozw2OsW4bQQxEhwLXkDrysGdEqRRgVShfQQq8wOr2jD0jSpXCLvwXbtKfADlFqgSwC/9ljqweZgYzQFnb/QGepYhA9jzmTc1WaSEtQpbFgjWATI00ZZtIckXx8q2Oe5yEByBy+RHOTcM+VVTadULsvxvRC/q8WTwgcWGD+Mnaqa0oy2gw2pKFzK+PzEsus5hP9AHojKslVynLlioVTBEN8cjDNnZoR1uMGTiZAAN47HxMtEkGUE9b8HWzkqNX5Lpk0yVziAJOs46rK1pG/xNuXLjz95fSDoJE5IqG23MAYPtWoeWPvfVtIV/Ng9oH3W0gGMPIOqd4MK4QZ55dV61gOb8Zxp7I9qayaGxp6Q91cmC0ZRdBwEQVHWzSAanlZwVWc9yljeTCeaHjBVvlPSLeyeBUT2rPdJegQI103jVS3uYkyIx1il6mslMDedZuOkwzolsagvPuQAfp7cYg7k9V1NOxfq64PNSvMdwONV4VYEmqlbpZy5OAakRKkjPnL4CBv5/OZRgoWHBmNbxB0LgB1I4vXFj93UoF2/0TPEsWwV9EhbIiTPqYoTHYoMn3enTDjmrFeDTIzaL1bUC/PBIMuF+vSSYSaxoVt90EO3Gu1zrMuMRGUk7Ffv3L+A931Gsb/yBoIgAAAABJRU5ErkJggg==",gt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEX6+fP8+/X+/ff///kbczPAAAACeElEQVQozxXHQUgUUQAG4P8936yzs6VvZNZmN9QxVxiF9OLBoOjtOC6rQq6ygXjI2fCQBdXBg4egtzFGdqkoI+zgBFbqkm3hQSxhFYLotOcubeKhOnVYoqQy+m4f5g5TvpX0xHLbLY9j8SMhJp+Jk4LfAUS2kVRIjILmnwGBTX42PhCVlDJQkIiy2nWAvaJ1h+oFIpJ0hMSYVbyyrgDWshcMpMyL1brPDQKWmduO+KTJ6XeXAMK9Yc3FpD7atyNwg6kt5XgFpLPhjUTFSYVn2abDiugGShwD8JTVRJVo/2ecuKtRb/qc4BK+9TboFfokog4T2Fn6Oqdnsjk90NMS76Rji6E0NmwkPBAZ4Xbkw8KoDAkAbEhkc78e9omxxgxg6qa5HvMv+UZbCV0qmHnSHKl5TxeA2XTCGWekR581mwC5crBH81PznASqB9va3TbkYAjJPLfg5uBfXaJgIgIBv9eessRIhxe7PA7kj6uUMeMaQ/OEQOYRaaHlqH2Gxwsl6E/pwVY5FH7uCypBZPKvDQyVziYBrAkMURe2MOOOxG/eQpp5PF+bFzUV5HtPj9GeiVSNZDELleifYTp9NAjsoiXg4cW+4ZORkdSMB/B74aAdjhsVakhgkugsbmqcDSLEoWp8zRjrux3tli6Q5uM3E+maT99Wy0RiP7tboiuRZle2c6CYeL2kcUc1KvPtQKucogMadKVTQOJYCeyCYlhQQ/Q7Etfd/vBygy9iqy+LyHeF46saCYvW6ingsbA9RBWtdi8GgUXW+oQx9/wP6bAAX1TWeV+CbShZDlQ9xT6SoSxZmKRAkmXb60kzEzkRF+Ccb94BGspGJoN/UzmyR4wjXHAAAAAASUVORK5CYII=",pt="data:image/png;base64,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",ft="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAALVBMVEXx58b168ny6Mjz6sn06sf27Mvw5sTz6cbw5cLy58T37svv47/168v37s7t4Ltrv0//AAAEjUlEQVQ4yw2Ty2sTURxGf3dmOqmPxb0zmaStCnfmZpL6gpmbxIpUSMZGrSJkxsZiVZimNVaqMklrUnWTRq2KIDFWWx+IFrIRFxXEB4KIgqu6EBdu7M6FIPg32PW3+DhwDmBaYrK56KP4HGIsvg/uvOV0wK+qgBMlO9BujuH4DSJlOseqV5a/BEF97gt0ChyIPqBhXI9BtqtIB8vJB/LdCQ3OVjaLNX0g7+OmoI4e7nkemAqX6o8vg0yyQAyQS7IfgvFbI+6QyI3R4KELxw7kwM2ooQfyQigYnwY5MZbMlHI1DvnQVCoVcrt+R+bO7vPDif3ybNajwqAAe443dpfDsPt379VMWZzGRuqM79mQF+DUz9nt74bQ8J/O80MtVR51U02JKKmTCvTzLVf+vuxP/aHnPo9+2bW+zVsJ0Y630/CrfzX+b+UL+7O68Rczv+7lrMh5etfKXvhc2rk6KforxuoO2xB2tcxKfeXHt18rHOiHI/0RRjW/YGRDkHiwo3nzqL60o58C/bgRuaj7vk+QOwOhpnFNdjuWpKMCGP8Yapu9Ty5FTHKQLGSEFikjd9ADwP9ciaNNjc5qMH6w50AF/LKOsOYqsOG9GjKgc7ZXolqntm6fysJ6Ma6ll2CiqmOgE6O7x1wXExklbeqMYcwsmJmOoigt8SBg2WfilDSsAZJcBxDcrqtBXzFQJqZNHfscyIhoZlygAtyYAceah+elrFbI+46gEHDGiW878Kj7JpWyfhg6iyRMymV1MKBSeVpfgLHIohyTojI6sRyK1VpcqzVZeEBLOnA9unhGKUXPJDYtV9Dxuz4iA5xSkSWhCJdAiJR9PHlvfvbntbrR14FDqUNRAYDJmSnv3oKxuz5+7fiblgVJyYLTbgUM05P7LESkoXvyWNfb0aUU6FZizgQIa25VqKQZqFrk6v6BsqqIHlQmkQ9KrBhkC20/DrFsAFEEYLjM+lj2wYHXCwnNvZQR42XJ2iVK+UBXnI+OBE6oXpUUHiQ1yg0MhA03iwGbnOdQYc1CMiPIPQrCQJFH4L4BMFktAtKd9PN5gnU2Gra4KuK+V+mjtBRpAGIqDVe4wnSnajiFGO5d7smvhVQEMEYwqshrENIEaY7YeblJYtsb3QhAHWZCEKK67swwPMKw0If1Ta+6DgHmlgPzcUTSbi3rrv1Y64/BYEMPQ5SDHUOR022B4QRF6xLUPAaPX/V4IDI5N2BMwx4LqO1uO4j6uW7NvM7lATqGAxY/ZHVgoGZbu7SvkNR75x6qGSB23FdouENVwN7sCbewTdsXGrrnQ5ZZKOCOFtMTIzxlPu6eYmtL+nMFmoK7OeXajn86r9sqWbfmvHC4IagE5qfCPGZvLSq5F55hHIxJFa4/vRxHBlz0og4TojU1l/MOHJX17lybdF0mQhFO44JYUNt3UA473IXw/iPfDWtKG5oFSXIF5iU/VnyDSjxxeDk3jAXRyVyGTNB9FxH9qcFDNJpVbt2y9LytUXkK7Py6+z1RezHQqnoY8XcLimmd8dCnBhQCuaGpJCq3SoIlmYvLz8UkWhJw7T8k+Db/DYEKwgAAAABJRU5ErkJggg==",mt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX48dr48Nf58tv379X17NJtIBxUAAACFUlEQVQ4y1XRUZakMAgF0Af2AiDWApDZgHZqAV1nZv9rGh7Rj7Y8McUFEg1wvcMESMNVD/neU8Xcaz7nYYkYlYO6Ti82PBI4BvIEg1aj3wKwRvIMgZsUy5LdhCawPFh1sZs4SrlyN9fQKpv8s5dgZ2eLyqqJiu+WkCmUEybXkm3INS01WAiv0PapJ0CZc0SJQUzcWnZYbOOY20iFD8Bk+/j2A3wNxH7GdShFYS5ff237kXh9I9zSkQmIAhOsOSVfJ6DIXTMDaPnzkRJ92S1BQQmXl5LdirgRLLDdcYqcGPwe3QN4xCBiGNbrqq9wpW1XCecChwaQdVOsRDpPCpeoolPdxeXp3WNB9PHVzWBHlygy4NJCCrFHREv6bDt0VGwJZASkpONmm1UseGeFKAQexgaAkrfYWl3AGxWOLL2AIMBNbCXpktmS3k3vHeYjGCPBa43wJTurO3ZFVpQSJdAZGLoHTyk1upkjxMEaIxum3iIARcCa5kSkFAW5fi1mUlL9eyOsaanFmOMruwvEdE3ZYzsRSzo5ewRLXyVPPEvknt8ij4DvCg2O7xOgBCUprEzV4z1WekSpUgI8DT2mrnSOXKRfQavwuKA1F+tFnMKdJSUpMA7wQAifWRkMgjUKKZE4lBl6MCM4B1pq1P4uIjDE6Pq6rL0FnW1nIFmta5vrSvq/Ch4tpqG/ZNyyWa5jZPktq81eYv8Bt5s4iFITOp4AAAAASUVORK5CYII=",vt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXN383Q4tDP4c/R5NEInCCXAAACVElEQVQozw3Hv2sTYRwH4M/79pJ7bZL2bXqtERJ97zjUpbZDhg6pfC8qibi8hLR0EaJ0EFxaCSWDxjfpj1zrYBcRBKE6SAfBJWsx9i8IQfdQxDlKtA6t2OnhQfN3lbG7ytYRywF8rVoPCNO0X2sQOKDpAnSDK2VwkHgmh5yLGT8qASt+2KofnNt2Xg1gf1UF8AoM6052cRMNaloLZb7RKQGrKKji2OefsZF+VqIvos5ZLVIZCX61JcwUdk56wASVkgQvzPfvmT2twTSwyYaC/Pl/UhAHorFhBgZtL6XdAZRp1tkPwC1NLa9CWs5prLhI85NBQsLdXvjDymG3/EbYfQhVNYqc3TtktQhWLY3ko0QsdMbSEp+64v0NfxyqLbIGdh6M2xHHlLBGqKTyQo4E/nebBgBfe1GpdeywYXc8CT7D3cKXuMXkBy4xN6o5OuKamYp3DVI6uccO9lxgd2CAlJgI2BGgaAgIJV/TYwKqu3WFccjbMuA+bVkWgS2bfnlRbD1Eb1sDyWMmjKYIBgGAWbqKRicfvzBkBIz3V5AKnguWdglQEysQsSuVzOg6ALy1pitA5ykGCsc857BRYcgCSZyFOdvoOigSGoPc5Ta73mgxshIcQE5sHMHd9D7yqITw7JO+GHVMxjhzYLcKPSEgmz3fU+BRy3iYNtiXLaBssCW8KguReqkQOTb3MStV0Ugt4U1eIs1RZWRII6Ww8xeNNItyGGQI4ZMlpg/3lQtkl2JFnBp1imRyFe0kK2Id3PCslMgiQNMS77gvFeDhG3cSkYvheeg/e7ClIh5oh+IAAAAASUVORK5CYII=",Bt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXh7eHl8eXj7+Pn8+eTbH1KAAACPElEQVQozxWPQWrbQABF/0xn3JFKQRTZOIuUsbCCbOgdRoYEOauxkYPcTRyTlPQWIxEltrsRwQ6hK9nEQek6F+gNTE/Q3qLLusv34cN7SH3mFicdYW4gNIhJWXPBRVXzjcFD0IqeU4o4PRbAIVjyico0vJpIifqPfL80QN9DAQY5ucRHE/hpHxBldXe9GilaHKcKMlj6pho2zXgkNdBl0oJ8kiF1DSiJF1ZHBJkQr0Dbux/5I42Zp4cFahJDFGeW6/QjBwmFY/Q7vZ2SnoOdW2parv/Cnm81+m0xrEfiVXQ3W4nOXIqVYi3l6AAQBwMFkViVBANMto4enXHPNTkHBB0oVj4r5vHzCWayrgBvxtygDlDB2CNDjd80ZInY69aKVYZcfJ8DW+fWuc+syEODALx+ojqoafHsthTI+ZW27PGpIeo/cR6YKcbqIuIFhHmBrzAovzIOOJk1ucvcDzrMRYGVBH2yvcAOf0KiKwfRovBI3tm/kW1eemtfNWwIIXE2mJNhvoszfmMBfRCv0OPwd2321uDW3nx2q/BDxFVeoN1g7a6Im8yRnoawa8kbdXnU0cHeTMxKfZGlJgvLb3sKsxgglQnDdAfvj9LUnqWRDo0GiUmPwyU7TAsD7wHeIW3Nfy1qVGKoE9NgJCdYCAexNRob9yCn4DAQmXtQuUtera6bEmTTXhZy6h856xi4mnEl6BI9mfISkLbtJyZIMJIAUd5ZOBEu88KRAk71yxfItj/hpIB0Errv4gO1os4/UICf+o3kkqwAAAAASUVORK5CYII=",kt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEX0/PTx+fH2/vbz+/P4//htSO9OAAAC5UlEQVQ4yyWT0QGjMAxDZTsDWKQDmJQBYrgBUsr+M517x0+LRWw9CyA+pC1YzndrMgHaNXVKQ+di13Of1qbur48nWhuRjj8i6ON8e7pNm7zyag/DBTfS9Z4Hup1fUuXMKY4HEE8QOHCByXkIkl7lDT239RtL9quO4JItmmhOAHXg45QuYKrQFLyGJcRvaTw6kQqZy6mkR6JAPFH/XqsQjEDRmUOA+MNLHGyMUT7AHApoAhjgjIJmCxy6XHdf648AWCdGe57IUDazCeTImQOY4/z+eVYVX2IjOw9RydeAeJwl79iGi4HpgQgHEchWraUZLtayu8scq0lHHHUKMY3Ml8hB7CS1jOckDLG9ccgNeX3124phOcjL9fPnWJhTXpLHeG9DRmHnTxHEaHakS2J51lwAJcUraNbuU7q4gMTDQj3Eripc/x+qFM5VEKAB1roQfAkX5/PxqnS2QpOrxfK1Zft0/omV5T+xCSBUAIbEIwUQgvAfxFE1O8dnk233+1UZiqJ1mAbsue6Yt8tF+yOrxC/YrUhzC4qPlE3EbR5hGKhhHdlrg7J9WunV7L7BcYQwAeE59u2tnN1c6gfVYrQiLSZ9OxZdWDXQq0+r0Pbarh3UqGCwauVvbiXuDsNxCtLDdW9rTF8oQYN4EoXXdfmwNguQP26n/tRjDeo+F2W7PjWtfSr6Bn/z+cXOLp4NnMV4RytvSW4B68m+XN9XfZTFGhO/S+cHTuTqZDC21ccA0N7QsePALaDQC3D1f94U9CWo+aq6BjB3v0rxIimBM12296M3aKPHjXLQE9KQKH4By8RHraJ3AgVto2r4xdFqlaPaiAHLl1ZF4P2pI6cYc+K8UZdcmxy7lqGc1IoPxLmIFuIeEZ6j2sQT88muEg1zwrEDTIX5U/ZmcsqfgVlBumiBLF4sAyhf9BFlXOPKLZ4H0iFb3VoHrGhtHTldKrOvP2/reu2zfV8CXMPqzRdlgd0a5eI7WwB/AYcgavcqxXWEAAAAAElFTkSuQmCC",ht="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXM2t7O3ODQ3uLR4OTDp25yAAACdUlEQVQozw3P70sTcQDH8c/3/M7NG+j35mnHwjwh4hRy/QFK3zvPNbeIG1koPZmxfj2IDAwihL53zj0JYisfmEHcZJZOiBUG60lZiI8T/ANusuftgQ+kCPIPeMP7hS5mUrV9c1g6MQCAEZ8tDLHwofImAGRlX+SZK3Vu9rRRPuO4PK6/9nA4GIATsxlODS+rdCMhkAZivpYV0LWoQHSLSA4NfUg+6mY+7BKL2++F9LvnrBDYm6JO9i/YO3i/HJTGQ4pdIV82TbEDFG6vGYCd4wZchgK5J2CrKTLE+Tx0v+YGlIbdWJFcQl4ptBN8fUJQN1MCJLcZLYwUVVo+famGGty8EXJF5ofOEDzcodT3/Fb0I5sHmc1ZG7CcSl8COgxlXx09jT05OafjCZLIHJhGIaU6wDZHsuMQ41wbdjmQXbhKnMq1zlXSYrjCnyZblqexA7fC8RxS74tq2P3OxSQwTuJSApH8OZLzBBp1pOe0i3rdyDUA47GySZ31YmC4EQYSXvFSvieORGBxXF9aeVtUWKGS9WMC4Z9Y2uXnJ2nCUXVMbPOYqNYNmGWWQ7Evr+BWC+a0JAMTImcq/S4Z5INdQMeuOqDIMa9beilxfA60iC6sP1INcPDpmHBW8drZHNmqwyddJtVje9q8WGUgWAOzmbU4FCQBFi8B2Wk6pickBnYhJMenmJGuRmtt2IoKq9NuFGbNFR99sHnvrnLsLysKANDIsxbp6RNMAsoDSKuRpMwZbAAzI68QatIjmZ0aImyM3O8/4e2MNlOHZomFsa/fLDsysliHS+nlYLQJMnynxrH8QO4PaAV2Li8B/+52UgeGIVNFYf8B1XG/kFSmLcUAAAAASUVORK5CYII=",Ct="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXh7vLf7PDj8PTm8/ecW+lZAAACZElEQVQozw2RsU8TUQCHfz3fw7MS87jeI7DdmSMpDEoHE+P0HqGkvRR8vb5XC4NpN2RQZqcK9xJkwtriekcggerC4OZADDiT+A+goxv/gfwB3zd8H/T6vYF/pTZkCSmDNd3CBEtmZJP4N+CvvhecDvmntKsvwB17rpbIRTLOEoYkj9KZzRUuJsuBQFwgptyJ3Y7EL4V+ud5LO1UnMeQSSObqisiISZkbQBlliP3qWSk3GPQXjxv6VF2BTDO4ySx1zhuJXbA2wBNJF4t5vH9keg6wu5NvUpLtXrZ3OHC9ZsgVcZdOl38PM1y/L6m8GRiErj4AqezUjHGatGGIgs5NJDHh8Ua1IuB4035haVT6SaYWMoQ0eJ3rB/Gpnr3fB49YAy1Wa21YKqAHOmAveVw6CCMGMZh5bGtVI7jnZaiQNbta1Z+285oSoKoRbta1KZ/1bBdKH/RIxv2pRVpkoCmvpr097RWoo0CpMlTWllIenSjECU8mV43mHx2fIRfH/pncrJm3+58BWdbSqCS07/yiQnvHiCG4ZPGRFeAtfreoOubyctzHvLNHhjNvIhukxQzjU5O6QdOEzUp1Ef4d98Pxz+IPYX0bcpnT52dbedfz8y7C4R89RV+MjJkuCCx7mWDt4eyK/62lQB55xXGJK7p8u6bgRv4hVHylelYGGFs64W94tng8sAIVqSRJBpqRA9rFvAysS+9ak8s7557pz5HR4qhCRmWgplpTRJ+bhYfSAMO8/YBucWPuSdmFFtOnuWqvV2NbF6CJnbhNDzEZ/T0XSDrUydzkZCG1z/oIEyUFYxW/KPXNfwopuHDcO04UAAAAAElFTkSuQmCC",yt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXm9PXq+Pno9vfs+vttWKBGAAACPElEQVQozw3RQWrbQACF4TfCMjPqZgIj4RRaxsZKE0PuMBZ2cLKaCI9RDAXFmJJknUWWI1O1UlamOMHJSjGkuFn3AD2Cr9CepDrAg+/xIxK4QwIqHHQkUhQ/WuphInVIFBojl8QXc012Tgq4RTtVHWVLZVFh1tEoI91uiN4joCqde8Ukn/zGM1B2W4ari2PtTwyw55Ld+Wways54qhGPyS6FzbIT3lIY8WwWdCq56Yolx6KmSKzoqrsCB5heAp4TGNQWJ1Pc6XlE5jQD5OlIX9I47A9uiUQcPQxcury/ToyxWJG/za6ki88crxKPocKS59Sl3EtBG7C89fCGflpfqoSzCeC4crioJA7F0V5+8MaSIk4qSCdwzpogmbqzEirVpGiS2dOVJvUuuqFEmhHao06KEpq+8lvHI14NJk3Qrmi9vBuRLwAz0qZB4hsDXQFXgtnlpDX3C6ug9BquSw/CYtwAzuTz5vuQNdr/YibhR68378ehZH30FSpjh71LpQkrsj+Q062h5WwZ5wlRoD6uQJy1DqvSYuCUapMBqT5YA4ZFw4KlWapxoUGlKWrx0eDQvmigu4WMYt97ruru98fYL8/0lG6CTOFcFWBhFK5gKw19h2JN808nh7xhkU6sWKLXdtkqBL6h+lULK5k19wFB/FldnGYf3LDeuf6IC2/MzJOSOP0qPxLqzaGIqtBcFIItrstkazONOkrc1D1czjuwEGESB4JJnjgSMN7PXAu7fZQpl1C236C+9mM4Af8P98Ch4R2TRl8AAAAASUVORK5CYII=",It="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXPz8/R0dHT09PU1NToNyAhAAACdElEQVQozw3NP0xTQQDH8d9d7sFrG+QeKVgQ4aoFCwFkYERyLY//0UB8GNGg1WAC0RBGJrzW4mCXQmpgvCYOwEAYiulSpYtza2KiW7s5FgNJFSV2/CzfL7RwpoJ20iadmgA8owOyaxmusKE44scBeb4vIv00dqYgmf6jzWcr7W6INbDQeZbQL9ytXeYgtFfzmW1Fek5msxJlwhyt6qDDxOLQzpVPompYrMPnEnhvLm7M5BxY5nowAj3zkydAkpC0FIG6g7AK+Ub25ybyNWVYwtpseP2rfrQwiGRpfqrnMuPeuvr2dA0p2YsHF2XghkrXKtZ8tLBjR7S2qIaYbKmyLd/QP+EogLjqqwNw5Lq1pDlMLkM5+gNoSvdq+Pxmz9/61EFq6GYM6GqaGvlN95zy3gsmEWI8K3k8OP9OmRLEPO6DP3Wv3g42COinJTZ33dcIvs4ESp6opMTjDs6mcYTEbFeUifuxh989yZrIx4lkpuixxz0nHLCekKbE17suKhYkMGhoYhTZtVBvg4bfq/1L1Im0AGMVpBFwumM0zwyuKiCMi5dqR4Flx47AGyF2xTbxqUdTwCH94BT3DozpLV5WuAL/N8rGtHKjotBOOuOtCJ9E21uqsyBoLOzaXbHPrK5PQBP+fBfeidvJAeMIAmzVt5IkJJ9DBWaZDAepYUhlQqHt0h72SJ3j8TZHom64f516xx9T5evgMPgwG82jZdJaJIDyWp6LAjOCclVyzNA3iTKzIULlBQEPaTXlPHok5gISclmyaWZlqY2aTHdRHpJOwTdDEQ3ZfKtbpclcNhyVClagmY+fIfyKukntPqBgnx5QvZHk/D/MK8JMClrSigAAAABJRU5ErkJggg==",bt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEXe3t7a2trc3Nzg4OCXP9lCAAACoklEQVQozwXBzU/TYBwA4N+QEr4CNbSFwcFuowSqMRvEAwShHWAYNsu7dS0dLnGUSWT4kZB4lGzE4VtcwgIDJqcOWLJxcv4BOoQZuCPxSNSD4WSWLJGL8XmAIiyo2RgJ4A1pxQQlOxRAszLTdnPu2oQGb05RC5slJld7ZAIfo4O44Bn1ud59F0BcjnYOa17Jhwc6EdiKettncsXjT1f8KUBZUW41pK0Jc1Az4dEV3rkkPBtDSZ83Blyt0kSf2PRjzIykoBwINisPbPPtljdVE9iAXRfUPkXLVIgYrCccp5g687NdZbcJ+xa5VE/HhTtT23IKsN5jj/pcUd0dTZNAqCVw72n4gOwnTOC0vvHfaauT8d9zAoRRfPpISZRVyUiw8ELzOG1b2DZpFzkSrHLhq52twDEdyZHwvp2j4uv/bjvOf23/AcEtTuJbY5Cp4YcAer1IGkUzOo2rn8LQOKjFJw3NTw24nprQXY5aF4wxcqcSdbFQ00H4xFl8Drx4X4CikvAM1tuR8bKIBCBoLnKN10KJG4zKAsc7c9WEB9gnCi6BhVjqoco6t20ILAJuVctvaEZK732cRHDRmGfuihOam0o2CHByUZ/epCcVlRs2wmCnMqsd6aSim3ibBJtm1LGyXW3Bb7tJCPlFtUG+SvPdeEUAB60lNdo+VQbLcwRNVtT68FsLcr1+NotgNihlpExS1V2SFgNbeC8bEhgm8sM17wSi6Us2gxVWJU/5GKBpandvfyYbU1yHCLpCgWGbbPXn40rehEsUXKIJr9DMKgICfjc4bl1YfvUhE/YIECGRqjCxSM9hrybAIkND5OeWfFZsXkxB+qDzb7pUQ3EfQ3Ml6EChEt3D+iS01VqC7EQ/Z/DuPQcz4yChoFQJce2Qr+NNAv0HxofmpXGqgHkAAAAASUVORK5CYII=",wt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAD1BMVEXm5ubo6Ojp6enr6+vt7e1FnZagAAACrklEQVQ4yx1SixUbMQgT3AKAFwDcAfzpBN1/qMrJS5w7bCQhC6IGSUGYQJd6Ox9ZPXi1AGJBavhUTT0JjYPGAab9WcDYIxsmlnxkayX8mhxCmKHA75az5cfRbWybEExiu08xDSgGym0mwuf3j4SvHeQxDJJzh2zp4iOlrD8iOb4SXyC1wiOLRTcnrje+nGamFeXVKWkmzbFIPChkmJ6Fg7mBpV8n+JGOVCd4jv1thThkjeQGNeafpeV3rsEWLfyWc8tC9jOv6FQ8rRzHOOVB+jCYEUAJpDvh8xHNFm/Tm5p5lw94Pp3NhtKEfQsGvnXhowdZE73hPwxKvjDd4i4PCdd0fe3W5fO8ktAsUAacLgstpUw60JCiPLg2XpkgiqPIYYXJd9ksGIT3q+LlevypzItvO+s0F1dBzVr2QDMUkYmuyGcrIS44mVJ7JVKwQXjYuBYp0Uetecbswzsikzu3gUR8bJC/C8Gd/NAzI/xdUGOYQQHDZ8X2d5XuzGRUiXAi9si5CRgoiToRZPtzLJkd0FUHRHZwJf0BHT1sE7gcnh0jmKKlSSF4/GBirGk5+K9NKlGDCfc9JtPhg78JdabH0YQRKNZnJ8tFnPfXHJb4xum1TTCeEmyEdbyEJLjznMLHuFD2Y9NEkSleIBs7SiCbblhgctVi9ch++kDYnn1C9DA5TvdPsToXM55wI6k+8eKT1blwPTqWb5CFJ+7dTBmab+KHy+xwNtItXhZNSpHD2fxnynrxG3ZBKRe8KBpXk11AnadlccEhr9w1nBBvBylNkv7A8eqpGBCDqhitmWQXBjjdS6idr/QjXWLDeMzMbVDoJuM8zN7WenMZWXgZ2vX3F01J3jHZbwk1LRP+DWEvDJtOUoh/AIaBUz5VpWyhuyx4QtgL/NmgC6kM/JvNe+R/C/5aL7BKIbYAAAAASUVORK5CYII=",St="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyBAMAAADsEZWCAAAAElBMVEUQERMODxESFBYWGBkaHB0eICLm6ozJAAACkUlEQVQ4yyWTUdLbMAiEASfvoOkBkBy/O5keIE0v8E/uf5h+68qZWALELgu2MG9PP9qyvCzTVhrrsPGOCjvTfXQZvtp/W3Gy6LCITqs4q/DZ+KYl76zKzHVYpY2wNY27nqN1sbLGcrLH3/ENH4oWlGctsDu8AO+HzTLlsYdh8MzP1m6YDMz0ACfcimvakBj+mwO/+5Uta5teOD379sxK1fUxmUhv8MU3jUT5gs26PMephFznkLcpQZ6/dPL9C/GWHcCxDN6oZhD5xBm5qoYBPA+PFE/H1tXDWcWl8TW7rS+4dUzAVy0BIrvC4/HcqW2TkG1HO8q9dC23INAg7NA4AFRFkDTM2lfELPyFzi1VddcpX2z0KjHBUDmdLNJ6dDps4ytrX+FPsZwE31wSL+6OWfHOAJ3+Y0Rk/MiKfmWNPg7oVP/U3Ck9FoCkC2gBpALOiqbMNTkOe8P4FWkTD2Y9Q3+5VmV0uLUJBl68U5uAK2Kl6QDXvLxbwweOL2sixW78uU8p0ysfc7cWrF1j6B1sPJ4WgclYSnJN1bzozrhEcFHmRzBkbJWqqdG+EYJXRFmT5jnLXPUNF6WBdoFbTxYsmDXVLU/WA7MExNc93sJS5hIXDeLxzMScHzdhKvEkibr6cQXYPrmtmTA7JcInISrTzRDvShTdka0uVGrsJAAR6tSn1sKziZtfKVjAxPrJsYgZO0bye+vKTZ/DgoAoLGNO6jYHimZYTL/3pLJHawquJukjBpfz8WOGVSVIWx9ywUfS5iENutidRM4NzkAmxgUSQ68xgNOU+ZLalr4TS2V+D2xqukZig+Z9DilR7Nouzwp1cp/3E5q6Rdlf08obKvAM4qZ6pMr+w3PmQALSSBfjyZn5DwrNRVbywBQiAAAAAElFTkSuQmCC",Et="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEUWGBkYGhsdHyAfISI1t/v6AAAB5ElEQVQozxXQsYoTURSA4f/EeycZsDgDdySDjihk38Hy3GWi2J2BCaziQhaiaB+tt9AFu1kwvYUPsIXNPoB9BAUfwAfwEUzKv/v4odGrroyp9/rUaC6rZ5skv5F8qPsfYYP+yKUMymmAEEeW55oUR4o8jr05KNzJ07yvB7w0KKfLwcQUSjfmMU0PJfPHFoEVU+ohNrcKMEzMQ23FDnVSI2dqtYWI7KlLu6vE4UnyvKc3SJuL7lBbeEEl42ItpGLjzIT8PRJCmkRjVpVpsbJFVN0687okJNZiHAr5Z7MV0BnGIDc+THM1zlbieBc1Fq+tH5BH+OpnbWkj40hSqC8Lw2TvFuF0SUFJCk2IytXbjeqcRAt6NHpnrUkUU4KRzZs8RCK8N/Akn2W04LwxMU/V7XK0bDyN2RxfDyx7I4h5vjZby72V8UnOWumZL3qtYc+8DTE0siSBMXGhywx2dMYPnQHbxdFZ7deiNGxCCtD/QWnbwDoGhRYPDzUdUA3krjpnkvdAgDN4ddLkEQSov9qjd42HaDjI34gEqS9TUueAk+sc4qg5ws407KQYKs8G1jv4xBlqBVk6cb4dISZIwVi1Jzu4+HLk6lyfUxkXvwy+1Q+4WVdHIhwfybZ6CWVhxMEhShOgsP/HOW0MvZJeFwAAAABJRU5ErkJggg==",xt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAgMAAABjUWAiAAAADFBMVEUWGBkYGhsdHyAfISI1t/v6AAAB5ElEQVQozxXQsYoTURSA4f/EeycZsDgDdySDjihk38Hy3GWi2J2BCaziQhaiaB+tt9AFu1kwvYUPsIXNPoB9BAUfwAfwEUzKv/v4odGrroyp9/rUaC6rZ5skv5F8qPsfYYP+yKUMymmAEEeW55oUR4o8jr05KNzJ07yvB7w0KKfLwcQUSjfmMU0PJfPHFoEVU+ohNrcKMEzMQ23FDnVSI2dqtYWI7KlLu6vE4UnyvKc3SJuL7lBbeEEl42ItpGLjzIT8PRJCmkRjVpVpsbJFVN0687okJNZiHAr5Z7MV0BnGIDc+THM1zlbieBc1Fq+tH5BH+OpnbWkj40hSqC8Lw2TvFuF0SUFJCk2IytXbjeqcRAt6NHpnrUkUU4KRzZs8RCK8N/Akn2W04LwxMU/V7XK0bDyN2RxfDyx7I4h5vjZby72V8UnOWumZL3qtYc+8DTE0siSBMXGhywx2dMYPnQHbxdFZ7deiNGxCCtD/QWnbwDoGhRYPDzUdUA3krjpnkvdAgDN4ddLkEQSov9qjd42HaDjI34gEqS9TUueAk+sc4qg5ws407KQYKs8G1jv4xBlqBVk6cb4dISZIwVi1Jzu4+HLk6lyfUxkXvwy+1Q+4WVdHIhwfybZ6CWVhxMEhShOgsP/HOW0MvZJeFwAAAABJRU5ErkJggg==",oe={themes:[{body:"#ede7da url("+dt+") repeat",content:"#ede7da url("+ut+") repeat",popup:"#ede7da url("+gt+") repeat"},{body:"#ede7da url("+pt+") repeat",content:"#ede7da url("+ft+") repeat",popup:"#ede7da url("+mt+") repeat"},{body:"#ede7da url("+vt+") repeat",content:"#ede7da url("+Bt+") repeat",popup:"#ede7da url("+kt+") repeat"},{body:"#ede7da url("+ht+") repeat",content:"#ede7da url("+Ct+") repeat",popup:"#ede7da url("+yt+") repeat"},{body:"#ebcece repeat",content:"#f5e4e4 repeat",popup:"#faeceb repeat"},{body:"#ede7da url("+It+") repeat",content:"#ede7da url("+bt+") repeat",popup:"#ede7da url("+wt+") repeat"},{body:"#ede7da url("+St+") repeat",content:"#ede7da url("+Et+") repeat",popup:"#ede7da url("+xt+") repeat"}],fonts:["Microsoft YaHei, PingFangSC-Regular, HelveticaNeue-Light, Helvetica Neue Light, sans-serif","PingFangSC-Regular, -apple-system, Simsun","Kaiti"]},Ut={class:"setting-list"},Dt={class:"theme-list"},Qt=["onClick"],Vt={key:0,class:"iconfont"},Ft={key:1,class:"moon-icon"},Pt={class:"font-list"},Mt=["onClick"],Lt={class:"font-list"},Rt={style:{"text-align":"right",margin:"0"}},Kt={class:"font-size"},zt={class:"resize"},Ht={class:"lang"},Ot={class:"letter-spacing"},Wt={class:"resize"},Nt={class:"lang"},Tt={class:"line-spacing"},Jt={class:"resize"},Gt={class:"lang"},qt={class:"paragraph-spacing"},Zt={class:"resize"},Yt={class:"resize"},jt={class:"lang"},Xt={key:0,class:"read-width"},_t={class:"resize"},$t={class:"lang"},eo={class:"paragraph-spacing"},to={class:"resize"},oo={class:"resize"},no={class:"lang"},so={class:"infinite-loading"},ao=ce({__name:"ReadSettings",setup(c){const s=me(),A=je(()=>ne.saveReadConfig(s.config),500);be(()=>s.config,()=>{A()},{deep:2});const f=l(()=>s.theme),n=l(()=>s.isNight),w=l(()=>f.value==6?"":""),m=[{background:"rgba(250, 245, 235, 0.8)"},{background:"rgba(245, 234, 204, 0.8)"},{background:"rgba(230, 242, 230, 0.8)"},{background:"rgba(228, 241, 245, 0.8)"},{background:"rgba(245, 228, 228, 0.8)"},{background:"rgba(224, 224, 224, 0.8)"},{background:"rgba(0, 0, 0, 0.5)"}],y=l(()=>({background:oe.themes[f.value].popup})),h=M=>{s.config.theme=M},F=D(["雅黑","宋体","楷书"]),P=M=>{s.config.font=M},x=l(()=>s.config.font),S=D(s.config.customFontName),V=D(!1),U=()=>{V.value=!1,s.config.font=-1,s.config.customFontName=S.value},C=()=>{V.value=!1,Re.prompt("请输入 字体网络链接","提示",{confirmButtonText:"确定",cancelButtonText:"取消",inputPattern:/^https?:.+$/,inputErrorMessage:"url 形式不正确",beforeClose:(M,e,N)=>{if(M==="confirm"){e.confirmButtonLoading=!0,e.confirmButtonText="下载中……";const Y=e.inputValue;if(typeof FontFace!="function")return R.error("浏览器不支持FontFace"),N();const _=new FontFace(S.value,`url("${Y}")`);document.fonts.add(_),_.load().then(function(){e.confirmButtonLoading=!1,R.info("字体加载成功!"),U(),N()}).catch(function(E){throw e.confirmButtonLoading=!1,e.confirmButtonText="确定",R.error("下载失败,请检查您输入的 url"),E})}else N()}})},I=l(()=>s.config.fontSize),b=()=>{s.config.fontSize<48&&(s.config.fontSize+=2)},v=()=>{s.config.fontSize>12&&(s.config.fontSize-=2)},u=l(()=>s.config.spacing),i=()=>{s.config.spacing.letter-=.01},d=()=>{s.config.spacing.letter+=.01},B=()=>{s.config.spacing.line-=.1},W=()=>{s.config.spacing.line+=.1},ve=()=>{s.config.spacing.paragraph-=.1},de=()=>{s.config.spacing.paragraph+=.1},Be=l(()=>s.config.readWidth),ue=()=>{s.config.readWidth+160+2*68>window.innerWidth||(s.config.readWidth+=160)},ke=()=>{s.config.readWidth>640&&(s.config.readWidth-=160)},he=l(()=>s.config.jumpDuration),j=()=>{s.config.jumpDuration+=100},Ce=()=>{s.config.jumpDuration!==0&&(s.config.jumpDuration-=100)},ge=l(()=>s.config.infiniteLoading),X=M=>{s.config.infiniteLoading=M};return(M,e)=>{const N=Ge,Y=Ye,_=Le;return g(),p("div",{class:Q(["settings-wrapper",{night:a(n),day:!a(n)}]),style:Z(a(y))},[e[51]||(e[51]=t("div",{class:"settings-title"},"设置",-1)),t("div",Ut,[t("ul",null,[t("li",Dt,[e[7]||(e[7]=t("i",null,"阅读主题",-1)),(g(),p(se,null,le(m,(E,L)=>t("span",{class:Q(["theme-item",{selected:a(f)==L}]),key:L,style:Z(E),ref_for:!0,ref:"themes",onClick:T=>h(L)},[L<6?(g(),p("em",Vt,"")):(g(),p("em",Ft,K(a(w)),1))],14,Qt)),64))]),t("li",Pt,[e[8]||(e[8]=t("i",null,"正文字体",-1)),(g(!0),p(se,null,le(a(F),(E,L)=>(g(),p("span",{class:Q(["font-item",{selected:a(x)==L}]),key:L,onClick:T=>P(L)},K(E),11,Mt))),128))]),t("li",Lt,[e[14]||(e[14]=t("i",null,"自定字体",-1)),H(N,{effect:"dark",content:"自定义的字体名称",placement:"top"},{default:z(()=>[qe(t("input",{type:"text",class:"font-item font-item-input","onUpdate:modelValue":e[0]||(e[0]=E=>fe(S)?S.value=E:null),placeholder:"请输入自定义的字体名称"},null,512),[[Ze,a(S)]])]),_:1}),H(_,{placement:"top",width:"270",trigger:"click",visible:a(V),"onUpdate:visible":e[4]||(e[4]=E=>fe(V)?V.value=E:null)},{reference:z(()=>[...e[12]||(e[12]=[t("span",{type:"text",class:"font-item"},"保存",-1)])]),default:z(()=>[e[13]||(e[13]=t("p",null," 已经安装在您的设备上的字体请确认输入的字体名称完整无误,或者从网络下载字体。 ",-1)),t("div",Rt,[H(Y,{size:"small",plain:"",onClick:e[1]||(e[1]=E=>V.value=!1)},{default:z(()=>[...e[9]||(e[9]=[q("取消",-1)])]),_:1}),H(Y,{type:"primary",size:"small",onClick:e[2]||(e[2]=E=>U())},{default:z(()=>[...e[10]||(e[10]=[q("确定",-1)])]),_:1}),H(Y,{type:"primary",size:"small",onClick:e[3]||(e[3]=E=>C())},{default:z(()=>[...e[11]||(e[11]=[q("网络下载",-1)])]),_:1})])]),_:1},8,["visible"])]),t("li",Kt,[e[20]||(e[20]=t("i",null,"字体大小",-1)),t("div",zt,[t("span",{class:"less",onClick:v},[...e[15]||(e[15]=[t("em",{class:"iconfont"},"",-1)])]),e[17]||(e[17]=t("b",null,null,-1)),e[18]||(e[18]=q()),t("span",Ht,K(a(I)),1),e[19]||(e[19]=t("b",null,null,-1)),t("span",{class:"more",onClick:b},[...e[16]||(e[16]=[t("em",{class:"iconfont"},"",-1)])])])]),t("li",Ot,[e[26]||(e[26]=t("i",null,"字距",-1)),t("div",Wt,[t("span",{class:"less",onClick:i},[...e[21]||(e[21]=[t("em",{class:"iconfont"},"",-1)])]),e[23]||(e[23]=t("b",null,null,-1)),e[24]||(e[24]=q()),t("span",Nt,K(a(u).letter.toFixed(2)),1),e[25]||(e[25]=t("b",null,null,-1)),t("span",{class:"more",onClick:d},[...e[22]||(e[22]=[t("em",{class:"iconfont"},"",-1)])])])]),t("li",Tt,[e[32]||(e[32]=t("i",null,"行距",-1)),t("div",Jt,[t("span",{class:"less",onClick:B},[...e[27]||(e[27]=[t("em",{class:"iconfont"},"",-1)])]),e[29]||(e[29]=t("b",null,null,-1)),e[30]||(e[30]=q()),t("span",Gt,K(a(u).line.toFixed(1)),1),e[31]||(e[31]=t("b",null,null,-1)),t("span",{class:"more",onClick:W},[...e[28]||(e[28]=[t("em",{class:"iconfont"},"",-1)])])])]),t("li",qt,[e[37]||(e[37]=t("i",null,"段距",-1)),t("div",Zt,[t("div",Yt,[t("span",{class:"less",onClick:ve},[...e[33]||(e[33]=[t("em",{class:"iconfont"},"",-1)])]),e[35]||(e[35]=t("b",null,null,-1)),t("span",jt,K(a(u).paragraph.toFixed(1)),1),e[36]||(e[36]=t("b",null,null,-1)),t("span",{class:"more",onClick:de},[...e[34]||(e[34]=[t("em",{class:"iconfont"},"",-1)])])])])]),a(s).miniInterface?pe("",!0):(g(),p("li",Xt,[e[43]||(e[43]=t("i",null,"页面宽度",-1)),t("div",_t,[t("span",{class:"less",onClick:ke},[...e[38]||(e[38]=[t("em",{class:"iconfont"},"",-1)])]),e[40]||(e[40]=t("b",null,null,-1)),e[41]||(e[41]=q()),t("span",$t,K(a(Be)),1),e[42]||(e[42]=t("b",null,null,-1)),t("span",{class:"more",onClick:ue},[...e[39]||(e[39]=[t("em",{class:"iconfont"},"",-1)])])])])),t("li",eo,[e[49]||(e[49]=t("i",null,"翻页速度",-1)),t("div",to,[t("div",oo,[t("span",{class:"less",onClick:Ce},[...e[44]||(e[44]=[t("em",{class:"iconfont"},"",-1)])]),e[46]||(e[46]=t("b",null,null,-1)),e[47]||(e[47]=q()),t("span",no,K(a(he)),1),e[48]||(e[48]=t("b",null,null,-1)),t("span",{class:"more",onClick:j},[...e[45]||(e[45]=[t("em",{class:"iconfont"},"",-1)])])])])]),t("li",so,[e[50]||(e[50]=t("i",null,"无限加载",-1)),(g(),p("span",{class:Q(["infinite-loading-item",{selected:a(ge)==!1}]),key:0,onClick:e[5]||(e[5]=E=>X(!1))},"关闭",2)),(g(),p("span",{class:Q(["infinite-loading-item",{selected:a(ge)==!0}]),key:1,onClick:e[6]||(e[6]=E=>X(!0))},"开启",2))])])])],6)}}}),io=Ae(ao,[["__scopeId","data-v-dd7cfcb2"]]),ro={class:"wrapper"},lo=["onClick"],co=ce({__name:"CatalogItem",props:{index:{},source:{},gotoChapter:{type:Function},currentChapterIndex:{}},setup(c){const s=c,A=n=>n==s.currentChapterIndex,f=l(()=>{const n=s.source;return"catas"in n?n.catas:[s.source]});return(n,w)=>(g(),p("div",ro,[(g(!0),p(se,null,le(a(f),m=>(g(),p("div",{class:Q(["cata-text",{selected:A(m.index)}]),key:m.url,onClick:y=>c.gotoChapter(m)},K(m.title),11,lo))),128))]))}}),Ao=Ae(co,[["__scopeId","data-v-a892cd6d"]]),uo=ce({__name:"PopCatalog",emits:["getContent"],setup(c,{emit:s}){const A=me(),{catalog:f,popCataVisible:n,miniInterface:w}=Ke(A),m=l(()=>A.theme),y=l(()=>A.theme),h=l(()=>({background:oe.themes[y.value].popup})),F=l(()=>{const C=f.value;if(w.value)return C;const I=Math.ceil(C.length/2),b=new Array(I);let v=0;for(;vA.readingBook.chapterIndex,set:C=>A.readingBook.chapterIndex=C}),S=l(()=>{const C=x.value;return w.value?C:Math.floor(C/2)});Xe(()=>{n.value&&P.value.scrollToIndex(S.value)});const V=s,U=C=>{const I=f.value.indexOf(C);x.value=I,A.setPopCataVisible(!1),A.setContentLoading(!0),A.saveBookProgress(),V("getContent",I)};return(C,I)=>(g(),p("div",{class:Q({"cata-wrapper":!0,visible:a(n)}),style:Z(a(h))},[I[0]||(I[0]=t("div",{class:"title"},"目录",-1)),H(a(_e),{style:{height:"300px",overflow:"auto"},class:Q({night:a(m),day:!a(m)}),ref_key:"virtualListRef",ref:P,"data-key":"index","wrap-class":"data-wrapper","item-class":"cata","data-sources":a(F),"data-component":Ao,"estimate-size":40,"extra-props":{gotoChapter:U,currentChapterIndex:a(x)}},null,8,["class","data-sources","extra-props"])],6))}}),go=Ae(uo,[["__scopeId","data-v-6cab38af"]]),po={class:"tools"},fo={class:"tools"},mo={key:0},vo={key:0},Bo={class:"content"},ko=["chapterIndex"],ho=ce({__name:"BookChapter",setup(c){const s=D(),{isLoading:A,loadingWrapper:f}=nt(s,"正在获取信息"),n=me(),{catalog:w,popCataVisible:m,readSettingsVisible:y,miniInterface:h,showContent:F,bookProgress:P,theme:x,isNight:S}=Ke(n),V=l({get:()=>n.readingBook.chapterPos,set:o=>n.readingBook.chapterPos=o}),U=l({get:()=>n.readingBook.chapterIndex,set:o=>n.readingBook.chapterIndex=o}),C=l({get:()=>n.readingBook.isSeachBook,set:o=>n.readingBook.isSeachBook=o});be(()=>n.readingBook,o=>{localStorage.setItem("readingRecent",JSON.stringify(o)),sessionStorage.setItem("chapterIndex",o.chapterIndex.toString()),sessionStorage.setItem("chapterPos",o.chapterPos.toString())},{deep:1});const I=l(()=>n.config.infiniteLoading);let b;const v=D();De(()=>{I.value?b==null||b.observe(v.value):b==null||b.disconnect()});const u=()=>{const o=T.value.slice(-1)[0].index;w.value.length-1>o&&(ae(o+1,!1),n.saveBookProgress())},i=o=>{if(!A.value)for(const{isIntersecting:r}of o){if(!r)return;u()}},d=l(()=>n.config.font>=0?oe.fonts[n.config.font]:n.config.customFontName),B=l(()=>n.config.fontSize+"px"),W=l(()=>oe.themes[x.value].body),ve=l(()=>oe.themes[x.value].content),de=l(()=>oe.themes[x.value].popup),Be=l(()=>h.value?window.innerWidth+"px":n.config.readWidth-130+"px"),ue=l(()=>h.value?window.innerWidth-33:n.config.readWidth-33),ke=l(()=>({background:W.value})),he=l(()=>({background:ve.value,width:Be.value})),j=D(!1),Ce=l(()=>({background:de.value,marginLeft:h.value?0:-(n.config.readWidth/2+68)+"px",display:h.value&&!j.value?"none":"block"})),ge=l(()=>({background:de.value,marginRight:h.value?0:-(n.config.readWidth/2+52)+"px",display:h.value&&!j.value?"none":"block"})),X=()=>{n.setMiniInterface(window.innerWidth<776);const o=n.config.readWidth;M(o)},M=o=>{n.miniInterface||(o<640&&(n.config.readWidth=640),o+2*68>window.innerWidth&&(n.config.readWidth-=160))};be(()=>n.config.readWidth,o=>M(o));const e=D(),N=D(),Y=()=>{te(e.value)},_=()=>{te(N.value)},E=$e(),L=()=>{E.push("/")},T=D([]),$=D(!0),ae=(o,r=!0,O=0)=>{r&&(n.setShowContent(!1),te(e.value,{duration:0}),we(o,O),T.value=[]);const J=n.readingBook.bookUrl,{title:G,index:re}=w.value[o];f(ne.getBookContent(J,re).then(k=>{if(k.data.isSuccess){const Te=k.data.data.split(/\n+/);T.value.push({index:o,content:Te,title:G}),r&&He(O)}else{R({message:k.data.errorMsg,type:"error"});const ee=[k.data.errorMsg];T.value.push({index:o,content:ee,title:G})}if(n.setContentLoading(!0),$.value=!1,n.setShowContent(!0),!k.data.isSuccess)throw k.data},k=>{const ee=["获取章节内容失败!"];throw T.value.push({index:o,content:ee,title:G}),n.setShowContent(!0),k}))},ze=D(),ye=D(),He=o=>{Me(()=>{ye.value.length===1&&ye.value[0].scrollToReadedLength(o)})},Oe=et(()=>n.saveBookProgress(),6e4),We=(o,r)=>{we(o,r),Oe()};De(()=>{var o;document.title=((o=w.value[U.value])==null?void 0:o.title)||document.title});const we=(o,r)=>{U.value=o,V.value=r},Se=()=>{const o=P.value;document.visibilityState=="hidden"&&o&&n.saveBookProgress()},Ee=()=>{n.setContentLoading(!0);const o=U.value+1;typeof w.value[o]<"u"?(R({message:"下一章",type:"info"}),ae(o),n.saveBookProgress()):R({message:"本章是最后一章",type:"error"})},xe=()=>{n.setContentLoading(!0);const o=U.value-1;typeof w.value[o]<"u"?(R({message:"上一章",type:"info"}),ae(o),n.saveBookProgress()):R({message:"本章是第一章",type:"error"})};let ie=!0;const Ie=o=>{if(ie)switch(o.key){case"ArrowLeft":o.stopPropagation(),o.preventDefault(),xe();break;case"ArrowRight":o.stopPropagation(),o.preventDefault(),Ee();break;case"ArrowUp":o.stopPropagation(),o.preventDefault(),document.documentElement.scrollTop===0?R.warning("已到达页面顶部"):(ie=!1,te(0-document.documentElement.clientHeight+100,{duration:n.config.jumpDuration,callback:()=>ie=!0}));break;case"ArrowDown":o.stopPropagation(),o.preventDefault(),document.documentElement.clientHeight+document.documentElement.scrollTop===document.documentElement.scrollHeight?R.warning("已到达页面底部"):(ie=!1,te(document.documentElement.clientHeight-100,{duration:n.config.jumpDuration,callback:()=>ie=!0}));break}},Ue=o=>{(o.key==="ArrowUp"||o.key==="ArrowDown")&&(o.preventDefault(),o.stopPropagation())};Fe(async()=>{await n.loadWebConfig();const o=sessionStorage.getItem("bookUrl"),r=sessionStorage.getItem("bookName"),O=sessionStorage.getItem("bookAuthor"),J=Number(sessionStorage.getItem("chapterIndex")||0),G=Number(sessionStorage.getItem("chapterPos")||0),re=sessionStorage.getItem("isSeachBook")==="true";if(Ve(o)||Ve(r)||O===null)return R.warning("书籍信息为空,即将自动返回书架页面..."),setTimeout(L,500);const k={bookUrl:o,name:r,author:O,chapterIndex:J,chapterPos:G,isSeachBook:re};X(),window.addEventListener("resize",X),f(n.loadWebCatalog(k).then(ee=>{n.setReadingBook(k),ae(J,!0,G),window.addEventListener("keyup",Ie),window.addEventListener("keydown",Ue),document.addEventListener("visibilitychange",Se),b=new IntersectionObserver(i,{rootMargin:"-100% 0% 20% 0%"}),I.value===!0&&b.observe(v.value),document.title="...",document.title=r+" | "+ee[J].title}))}),Pe(()=>{window.removeEventListener("keyup",Ie),window.removeEventListener("keydown",Ue),window.removeEventListener("resize",X),document.removeEventListener("visibilitychange",Se),y.value=!1,m.value=!1,b==null||b.disconnect(),b=null});const Ne=async()=>{const o=n.readingBook;o.isSeachBook===!0&&await Re.confirm(`是否将《${o.name}》放入书架?`,"放入书架",{confirmButtonText:"确认",cancelButtonText:"否",type:"info",closeOnHashChange:!1}).then(()=>{C.value=!1}).catch(async()=>{await ne.deleteBook(o)}).finally(()=>sessionStorage.removeItem("isSeachBook"))};return tt(async(o,r,O)=>{window.removeEventListener("keyup",Ie),await Ne(),O()}),(o,r)=>{const O=go,J=Le,G=io,re=At;return g(),p("div",{class:Q(["chapter-wrapper",{night:a(S),day:!a(S)}]),style:Z(a(ke)),onClick:r[2]||(r[2]=k=>j.value=!a(j))},[t("div",{class:"tool-bar",style:Z(a(Ce))},[t("div",po,[H(J,{placement:"right",width:a(ue),trigger:"click","show-arrow":!1,visible:a(m),"onUpdate:visible":r[0]||(r[0]=k=>fe(m)?m.value=k:null),"popper-class":"pop-cata"},{reference:z(()=>[t("div",{class:Q(["tool-icon",{"no-point":!1}])},[...r[3]||(r[3]=[t("div",{class:"iconfont"},"",-1),t("div",{class:"icon-text"},"目录",-1)])])]),default:z(()=>[H(O,{onGetContent:ae,class:"popup"})]),_:1},8,["width","visible"]),H(J,{placement:"right",width:a(ue),trigger:"click","show-arrow":!1,visible:a(y),"onUpdate:visible":r[1]||(r[1]=k=>fe(y)?y.value=k:null),"popper-class":"pop-setting"},{reference:z(()=>[t("div",{class:Q(["tool-icon",{"no-point":a($)}])},[...r[4]||(r[4]=[t("div",{class:"iconfont"},"",-1),t("div",{class:"icon-text"},"设置",-1)])],2)]),default:z(()=>[H(G,{class:"popup"})]),_:1},8,["width","visible"]),t("div",{class:"tool-icon",onClick:L},[...r[5]||(r[5]=[t("div",{class:"iconfont"},"",-1),t("div",{class:"icon-text"},"书架",-1)])]),t("div",{class:Q(["tool-icon",{"no-point":a($)}]),onClick:Y},[...r[6]||(r[6]=[t("div",{class:"iconfont"},"",-1),t("div",{class:"icon-text"},"顶部",-1)])],2),t("div",{class:Q(["tool-icon",{"no-point":a($)}]),onClick:_},[...r[7]||(r[7]=[t("div",{class:"iconfont"},"",-1),t("div",{class:"icon-text"},"底部",-1)])],2)])],4),t("div",{class:"read-bar",style:Z(a(ge))},[t("div",fo,[t("div",{class:Q(["tool-icon",{"no-point":a($)}]),onClick:xe},[r[8]||(r[8]=t("div",{class:"iconfont"},"",-1)),a(h)?(g(),p("span",mo,"上一章")):pe("",!0)],2),t("div",{class:Q(["tool-icon",{"no-point":a($)}]),onClick:Ee},[a(h)?(g(),p("span",vo,"下一章")):pe("",!0),r[9]||(r[9]=t("div",{class:"iconfont"},"",-1))],2)])],4),r[10]||(r[10]=t("div",{class:"chapter-bar"},null,-1)),t("div",{class:"chapter",ref_key:"content",ref:s,style:Z(a(he))},[t("div",Bo,[t("div",{class:"top-bar",ref_key:"top",ref:e},null,512),(g(!0),p(se,null,le(a(T),k=>(g(),p("div",{key:k.index,chapterIndex:k.index,ref_for:!0,ref_key:"chapter",ref:ze},[a(F)?(g(),ot(re,{key:0,ref_for:!0,ref_key:"chapterRef",ref:ye,chapterIndex:k.index,contents:k.content,title:k.title,spacing:a(n).config.spacing,fontSize:a(B),fontFamily:a(d),onReadedLengthChange:We},null,8,["chapterIndex","contents","title","spacing","fontSize","fontFamily"])):pe("",!0)],8,ko))),128)),t("div",{class:"loading",ref_key:"loading",ref:v},null,512),t("div",{class:"bottom-bar",ref_key:"bottom",ref:N},null,512)])],4)],6)}}}),bo=Ae(ho,[["__scopeId","data-v-fff9fad7"]]);export{bo as default}; diff --git a/app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css b/app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css new file mode 100755 index 000000000..f08ee9e6e --- /dev/null +++ b/app/src/main/assets/web/vue/assets/BookShelf-00b2QCsd.css @@ -0,0 +1 @@ +@charset "UTF-8";.books-wrapper[data-v-0f5f0160]{overflow:auto}.books-wrapper .wrapper[data-v-0f5f0160]{display:grid;grid-template-columns:repeat(auto-fill,380px);justify-content:space-around;grid-gap:10px}.books-wrapper .wrapper .book[data-v-0f5f0160]{-webkit-user-select:none;user-select:none;display:flex;cursor:pointer;margin-bottom:18px;padding:24px;width:360px;flex-direction:row;justify-content:space-around}.books-wrapper .wrapper .book .cover-img[data-v-0f5f0160],.books-wrapper .wrapper .book .cover-img .cover[data-v-0f5f0160]{width:84px;height:112px}.books-wrapper .wrapper .book .info[data-v-0f5f0160]{display:flex;flex-direction:column;justify-content:space-around;align-items:left;height:112px;margin-left:20px;flex:1;overflow:hidden}.books-wrapper .wrapper .book .info .name[data-v-0f5f0160]{width:fit-content;font-size:16px;font-weight:700;color:#33373d}.books-wrapper .wrapper .book .info .sub[data-v-0f5f0160]{display:flex;flex-direction:row;align-items:baseline;justify-content:var(--v2a51eeb0);font-size:12px;font-weight:600;color:#6b6b6b}.books-wrapper .wrapper .book .info .sub .tags[data-v-0f5f0160] .el-tag{margin-right:.5em}.books-wrapper .wrapper .book .info .sub .update-info[data-v-0f5f0160]{display:flex}.books-wrapper .wrapper .book .info .sub .update-info .dot[data-v-0f5f0160]{margin:0 7px}.books-wrapper .wrapper .book .info .intro[data-v-0f5f0160],.books-wrapper .wrapper .book .info .dur-chapter[data-v-0f5f0160],.books-wrapper .wrapper .book .info .last-chapter[data-v-0f5f0160]{color:#969ba3;font-size:13px;margin-top:3px;font-weight:500;word-wrap:break-word;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;line-clamp:1;text-align:left}.books-wrapper .wrapper .book[data-v-0f5f0160]:hover{background:#0000001a;transition-duration:.5s}.books-wrapper .wrapper[data-v-0f5f0160]:last-child{margin-right:auto}.books-wrapper[data-v-0f5f0160]::-webkit-scrollbar{width:0!important}@media screen and (max-width: 750px){.books-wrapper .wrapper[data-v-0f5f0160]{display:flex;flex-direction:column}.books-wrapper .wrapper .book[data-v-0f5f0160]{box-sizing:border-box;width:100%;margin-bottom:0;padding:10px 20px}}body{padding:0;margin:0;height:100vh}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;margin:0;height:100%}@font-face{font-family:FZZCYSK;src:local("☺"),url(./shelffont-D-W4UqG-.ttf);font-style:normal;font-weight:400}.index-wrapper[data-v-5061e9c0]{height:100%;width:100%;display:flex;flex-direction:row}.index-wrapper .navigation-wrapper[data-v-5061e9c0]{width:260px;min-width:260px;padding:48px 36px;background-color:#f7f7f7}.index-wrapper .navigation-wrapper .navigation-title[data-v-5061e9c0]{font-size:24px;font-weight:500;font-family:FZZCYSK}.index-wrapper .navigation-wrapper .navigation-sub-title[data-v-5061e9c0]{font-size:16px;font-weight:300;font-family:FZZCYSK;margin-top:16px;color:#b1b1b1}.index-wrapper .navigation-wrapper .search-wrapper .search-input[data-v-5061e9c0]{border-radius:50%;margin-top:24px}.index-wrapper .navigation-wrapper .search-wrapper .search-input[data-v-5061e9c0] .el-input__wrapper{border-radius:50px;border-color:#e3e3e3}.index-wrapper .navigation-wrapper .bottom-wrapper[data-v-5061e9c0]{display:flex;flex-direction:column}.index-wrapper .navigation-wrapper .recent-wrapper[data-v-5061e9c0]{margin-top:36px}.index-wrapper .navigation-wrapper .recent-wrapper .recent-title[data-v-5061e9c0]{font-size:14px;color:#b1b1b1;font-family:FZZCYSK}.index-wrapper .navigation-wrapper .recent-wrapper .reading-recent[data-v-5061e9c0]{margin:18px 0}.index-wrapper .navigation-wrapper .recent-wrapper .reading-recent .recent-book[data-v-5061e9c0]{font-size:10px;cursor:pointer}.index-wrapper .navigation-wrapper .setting-wrapper[data-v-5061e9c0]{margin-top:36px}.index-wrapper .navigation-wrapper .setting-wrapper .setting-title[data-v-5061e9c0]{font-size:14px;color:#b1b1b1;font-family:FZZCYSK}.index-wrapper .navigation-wrapper .setting-wrapper .no-point[data-v-5061e9c0]{pointer-events:none}.index-wrapper .navigation-wrapper .setting-wrapper .setting-connect[data-v-5061e9c0]{font-size:8px;margin-top:16px;cursor:pointer}.index-wrapper .navigation-wrapper .bottom-icons[data-v-5061e9c0]{position:fixed;bottom:0;height:120px;width:260px;align-items:center;display:flex;flex-direction:row}.index-wrapper .shelf-wrapper[data-v-5061e9c0]{padding:48px;width:100%;display:flex;flex-direction:column;box-sizing:border-box;overflow:hidden}@media screen and (max-width: 750px){.index-wrapper[data-v-5061e9c0]{overflow-x:hidden;flex-direction:column}.index-wrapper .navigation-wrapper[data-v-5061e9c0]{padding:20px 24px;box-sizing:border-box;width:100%}.index-wrapper .navigation-wrapper .navigation-title-wrapper[data-v-5061e9c0]{white-space:nowrap;display:flex;justify-content:space-between;align-items:flex-end}.index-wrapper .navigation-wrapper .bottom-wrapper[data-v-5061e9c0]{flex-direction:row}.index-wrapper .navigation-wrapper .bottom-wrapper[data-v-5061e9c0]>*{flex-grow:1;margin-top:18px}.index-wrapper .navigation-wrapper .bottom-wrapper>* .reading-recent[data-v-5061e9c0],.index-wrapper .navigation-wrapper .bottom-wrapper>* .setting-item[data-v-5061e9c0]{margin-bottom:0}.index-wrapper .navigation-wrapper .bottom-icons[data-v-5061e9c0]{display:none}.index-wrapper .shelf-wrapper[data-v-5061e9c0]{padding:0;flex-grow:1}.index-wrapper .shelf-wrapper[data-v-5061e9c0] .el-loading-spinner{display:none}}.night .navigation-wrapper[data-v-5061e9c0]{background-color:#454545}.night .navigation-wrapper .navigation-title[data-v-5061e9c0]{color:#aeaeae}.night .navigation-wrapper .search-wrapper .search-input .el-input__wrapper[data-v-5061e9c0]{background-color:#454545}.night .navigation-wrapper .search-wrapper .search-input .el-input__inner[data-v-5061e9c0]{color:#b1b1b1}.night[data-v-5061e9c0] .shelf-wrapper{background-color:#161819} diff --git a/app/src/main/assets/web/vue/assets/BookShelf-DIQtBULC.js b/app/src/main/assets/web/vue/assets/BookShelf-DIQtBULC.js new file mode 100755 index 000000000..0bda07682 --- /dev/null +++ b/app/src/main/assets/web/vue/assets/BookShelf-DIQtBULC.js @@ -0,0 +1 @@ +import{d as H,a4 as j,o as d,e as h,h as e,F as N,R as W,z as p,c as $,w as M,g as P,a5 as J,P as x,u as o,C as V,Q as ee,a6 as te,a7 as se,U as oe,f as I,O as ae,G as ne,H as re,D as ie,y as b,M as C,s as le,n as z,a8 as ce}from"./vendor-KSDcS24u.js";import{d as de,A as B,i as ue,_ as O,u as he,a as pe,l as ge,s as ve,p as me,b as D,v as fe}from"./index-Wr40-hHf.js";import{u as _e}from"./loading-C4J6hIxs.js";const we={class:"books-wrapper"},Be={class:"wrapper"},ke=["onClick"],ye={class:"cover-img"},Ae=["src"],Se={class:"info"},xe={class:"name"},Ie={class:"sub"},Ce={class:"author"},Re={key:0,class:"tags"},Ee={key:1,class:"update-info"},Le={class:"size"},be={class:"date"},ze={key:0,class:"intro"},Me={key:1,class:"dur-chapter"},Pe={class:"last-chapter"},Ve=H({__name:"BookItems",props:{books:{},isSearch:{type:Boolean}},emits:["bookClick"],setup(_,{emit:i}){j(g=>({v2a51eeb0:o(A)}));const k=_,R=i,a=g=>R("bookClick",g),y=({bookUrl:g,coverUrl:r})=>r===void 0?B.getProxyCoverUrl(g):ue(r)?B.getProxyCoverUrl(r):r,E=g=>{const r=g.target;r.src=B.getProxyCoverUrl(r.src)},A=V(()=>k.isSearch?"space-between":"flex-start");return(g,r)=>{const v=J;return d(),h("div",we,[e("div",Be,[(d(!0),h(N,null,W(_.books,n=>{var l;return d(),h("div",{class:"book",key:n.bookUrl,onClick:f=>a(n)},[e("div",ye,[(d(),h("img",{class:"cover",src:y(n),key:n.coverUrl,onErrorOnce:E,alt:"",loading:"lazy"},null,40,Ae))]),e("div",Se,[e("div",xe,p(n.name),1),e("div",Ie,[e("div",Ce,p(n.author),1),_.isSearch?(d(),h("div",Re,[(d(!0),h(N,null,W((l=n.kind)==null?void 0:l.split(",").slice(0,2),f=>(d(),$(v,{key:f},{default:M(()=>[P(p(f),1)]),_:2},1024))),128))])):x("",!0),_.isSearch?x("",!0):(d(),h("div",Ee,[r[0]||(r[0]=e("div",{class:"dot"},"•",-1)),e("div",Le,"共"+p(n.totalChapterNum)+"章",1),r[1]||(r[1]=e("div",{class:"dot"},"•",-1)),e("div",be,p(o(de)(n.lastCheckTime)),1)]))]),_.isSearch?(d(),h("div",ze,p(n.intro),1)):x("",!0),_.isSearch?x("",!0):(d(),h("div",Me," 已读:"+p(n.durChapterTitle),1)),e("div",Pe,"最新:"+p(n.latestChapterTitle),1)])],8,ke)}),128))])])}}}),Te=O(Ve,[["__scopeId","data-v-0f5f0160"]]),Ue="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAECUlEQVRYR7WXTYhcRRDHq3pY9yKrYBQ8KBsjgvHgwRhiQBTjYZm4Xe8NusawhwS/o9GLoKhgBGPAgJd1NdGIXwtZTbRf9Rqzl6gHTVyDeIkIgnEOghAM6oKHzTJd0sO8Zaa338zb7NjwmJn++Ndv+lVVVyOsoM3Ozl69sLBAiHiDc26NUuoKv9w5d14p9aeI/DI4OMgjIyN/lJXFMhOttQ8BgBaR0TLzEXEGAKzW+lCv+V0BmLmGiLtF5M5eQrFxRPxaRCaI6LOi9YUAzPwGADxxMYYjayaJ6MkoZKyTmU8AwF19Mp7LfElEW0LNZTvAzIcBYFufjedy00T0QLt2B4AxZo9S6qX/yXhT1jn3cpqme3IbSwDM/DgAvNlu3Dm3Uyl1HAA2IOJ2EdleEu5Io9H4EBHPVCqVLSISRsMuInrLazUBpqamhoaGhr4TkRsDgLVpmtbzPmPMLQBwOwD4vvzxw8P5IyJztVrtVL4my7L1iPhTx7Yj/jw/P79pfHx8vgmQZdkLiPhK+O8GBgauqVarv5f819FpxpjLlVJ/hYMi8mKSJHubAMz8KwBcF1EYI6IjqwRIlFImonGWiNZhlmVVRDxWYGTVAMx8HwB8EtMXka1orT0gIo9GJrxNRLH+FW8IMx8EgEeW5QDEgx5gTkQ2Bk7yr9b60hVb6rKAmc8BwJWBne+x4P3XiWhtPwGstV9FzpSzHuBvALgsMHaaiDp2ZbUwWZZNIuKuQOcfD7AAAJeEcaq1Xr9ao+3rmdknnscCzQse4LdWEukYazQaa2q12vl+QTDztwCwOdCr+zA8iYi3RQwREdl+ADDz9QDwIwB0OLaInPJRcEhEHoyEyAmt9d39ALDW2lg1hYjv+lfgC4WJgkTxcJIkPcuqbpC+qgKATwvm7PYAGwDgdBeRZ4notYvZCWPMDqXUe13W3to8C6y10yJyv//u6zj/2R6ziPiRiBwt6xPMrBExFZEdRcYR8WOt9bb8MNoKAJ+3Jvtwed05d4dSKtz+c4h4VGsdrRWttZMici8AXFVix+4homNLBUmWZQcQMc/9x4mommXZ84i4t11MKbV5dHR06bxvH5uZmbnZOfdN6O0RmMNE1CxulgCstdeKyBcAcFPrVTyltZ4wxiSVSuXplkhda72zh9P1rClFZFOSJHMdAP5Hq3rxR6eH+IGIvIOuqFlr94nIc10WdRzxy6riAMJnr2nn3JlcME3TppMWNWvtfhF5pmB8WX0RvZgEEEtaYUUbM2KtfUdE/FUubNHipvBmZIxZp5TaDwBprlQGIHLqzSHiPq01x4B7Xk6Z2d8TfDwPlwFozfd1f90598Hi4uKrY2NjFwrzQVkP81nNi/byAWOMv8gOp2n6fhnt/wDqJrRWLmhIrwAAAABJRU5ErkJggg==",Ne={class:"navigation-wrapper"},We={class:"search-wrapper"},De={class:"bottom-wrapper"},He={class:"recent-wrapper"},Je={class:"reading-recent"},Oe={class:"setting-wrapper"},Ze={class:"setting-item"},Fe={class:"bottom-icons"},Ke={href:"https://github.com/gedoor/legado_web_bookshelf",target:"_blank"},Ye={class:"bottom-icon"},Qe=["src"],qe=H({__name:"BookShelf",setup(_){const i=he(),k=V(()=>i.isNight),R=s=>{try{s!==void 0&&i.setConfig(s)}catch{z.info("阅读界面配置解析错误")}},a=C({name:"尚无阅读记录",author:"",bookUrl:"",chapterIndex:0,chapterPos:0,isSeachBook:!1}),y=C(),{showLoading:E,closeLoading:A,loadingWrapper:g,isLoading:r}=_e(y,"正在获取书籍信息"),v=le([]),n=V(()=>i.shelf),l=C(""),f=C(!1);ee(()=>{if(!(f.value&&l.value!="")){if(f.value=!1,v.value=[],l.value==""){v.value=n.value;return}v.value=n.value.filter(s=>s.name.includes(l.value)||s.author.includes(l.value))}});const T=()=>{l.value!=""&&(v.value=[],i.clearSearchBooks(),E(),f.value=!0,B.search(l.value,s=>{r&&A();try{i.setSearchBooks(s),v.value=i.searchBooks}catch(t){throw z.error("后端数据错误"),t}},()=>{A(),v.value.length==0&&z.info("搜索结果为空")}))},S=pe(),{connectStatus:Z,connectType:F,newConnect:K}=te(S),Y=()=>{ce.prompt("请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)","提示",{confirmButtonText:"确定",cancelButtonText:"取消",inputPlaceholder:ge,inputValidator:s=>fe(s),inputErrorMessage:"输入的格式不对",beforeClose:(s,t,m)=>{if(s==="confirm"){S.setNewConnect(!0),t.confirmButtonLoading=!0,t.confirmButtonText="校验中……";const c=new URL(t.inputValue).toString();B.getReadConfig(c).then(function(u){S.setNewConnect(!1),R(u),t.confirmButtonLoading=!1,i.clearSearchBooks(),ve(...me(c)),c===location.origin?localStorage.removeItem(D):localStorage.setItem(D,c),i.loadBookShelf(),m()}).catch(function(u){throw S.setNewConnect(!1),t.confirmButtonLoading=!1,t.confirmButtonText="确定",u})}else m()}})},Q=se(),q=async s=>{const t="respondTime"in s;t&&await B.saveBook(s);const{bookUrl:m,name:c,author:u,durChapterIndex:w=0,durChapterPos:L=0}=s;U(m,c,u,w,L,t)},U=(s,t,m,c,u,w=!1,L=!1)=>{if(t!=="尚无阅读记录"){if(L&&n.value.every(X=>X.bookUrl!==s)){l.value=t,T();return}sessionStorage.setItem("bookUrl",s),sessionStorage.setItem("bookName",t),sessionStorage.setItem("bookAuthor",m),sessionStorage.setItem("chapterIndex",String(c)),sessionStorage.setItem("chapterPos",String(u)),sessionStorage.setItem("isSeachBook",String(w)),a.value={name:t,author:m,bookUrl:s,chapterIndex:c,chapterPos:u,isSeachBook:w},localStorage.setItem("readingRecent",JSON.stringify(a.value)),Q.push({path:"/chapter"})}},G=async()=>{await i.loadWebConfig(),await i.saveBookProgress(),await i.loadBookShelf()};return oe(()=>{const s=localStorage.getItem("readingRecent");s!=null&&(a.value=JSON.parse(s),typeof a.value.chapterIndex>"u"&&(a.value.chapterIndex=0)),g(G())}),(s,t)=>{const m=ie,c=J,u=Te;return d(),h("div",{class:b({"index-wrapper":!0,night:o(k),day:!o(k)})},[e("div",Ne,[t[4]||(t[4]=e("div",{class:"navigation-title-wrapper"},[e("div",{class:"navigation-title"},"阅读"),e("div",{class:"navigation-sub-title"},"清风不识字,何故乱翻书")],-1)),e("div",We,[I(m,{placeholder:"搜索书籍,在线书籍自动加入书架",modelValue:o(l),"onUpdate:modelValue":t[0]||(t[0]=w=>re(l)?l.value=w:null),class:"search-input","prefix-icon":o(ne),onKeyup:ae(T,["enter"])},null,8,["modelValue","prefix-icon"])]),e("div",De,[e("div",He,[t[2]||(t[2]=e("div",{class:"recent-title"},"最近阅读",-1)),e("div",Je,[I(c,{type:o(a).name=="尚无阅读记录"?"warning":"primary",class:b(["recent-book",{"no-point":o(a).bookUrl==""}]),size:"large",onClick:t[1]||(t[1]=w=>U(o(a).bookUrl,o(a).name,o(a).author,o(a).chapterIndex,o(a).chapterPos,o(a).isSeachBook,!0))},{default:M(()=>[P(p(o(a).name),1)]),_:1},8,["type","class"])])]),e("div",Oe,[t[3]||(t[3]=e("div",{class:"setting-title"},"基本设定",-1)),e("div",Ze,[I(c,{type:o(F),size:"large",class:b(["setting-connect",{"no-point":o(K)}]),onClick:Y},{default:M(()=>[P(p(o(Z)),1)]),_:1},8,["type","class"])])])]),e("div",Fe,[e("a",Ke,[e("div",Ye,[e("img",{src:o(Ue),alt:""},null,8,Qe)])])])]),e("div",{class:"shelf-wrapper",ref_key:"shelfWrapper",ref:y},[I(u,{books:o(v),onBookClick:q,isSearch:o(f)},null,8,["books","isSearch"])],512)],2)}}}),$e=O(qe,[["__scopeId","data-v-5061e9c0"]]);export{$e as default}; diff --git a/app/src/main/assets/web/vue/assets/iconfont-PstzbNMW.woff b/app/src/main/assets/web/vue/assets/iconfont-PstzbNMW.woff new file mode 100755 index 0000000000000000000000000000000000000000..4af734b10ac8c3245c5c6ecca85b1ab7bf14e9d5 GIT binary patch literal 30972 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.el-button[data-v-096adf6d]{margin:4px;padding:1em;width:6em}.hotkeys-item .title[data-v-096adf6d]{width:5em;display:flex;justify-content:flex-end;margin-right:1em}.hotkeys-item .hotkeys-item__content[data-v-096adf6d]{display:flex;flex-wrap:wrap;flex:1}.hotkeys-item .hotkeys-item__content div[data-v-096adf6d]{margin-bottom:1em}.hotkeys-item .hotkeys-item__content span[data-v-096adf6d]{margin:.5em}[data-v-c07c5146] .el-tab-pane{height:calc(100vh - 55px);padding-top:15px;padding-right:5px;overflow-y:auto}[data-v-c07c5146] .el-tabs__header{margin:0}kbd{align-items:center;background:#7d7d7d1a;border-radius:3px;border:0;padding:4px 5px;font-weight:700;box-shadow:inset 0 -2px #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px #1e235a66}code{border-radius:4px;padding:.15rem .5rem;background-color:var(--el-fill-color-light);transition:color .25s,background-color .5s;font-size:14px}body{padding:0;margin:0}.el-tabs__header{position:sticky;top:0;z-index:2}.editor[data-v-f2c47af3]{display:flex;height:100vh;overflow:hidden}.editor .left[data-v-f2c47af3]{flex:1;margin-left:20px}.editor .right[data-v-f2c47af3]{flex:1;width:360px;margin-right:20px} diff --git a/app/src/main/assets/web/vue/assets/index-Wr40-hHf.js b/app/src/main/assets/web/vue/assets/index-Wr40-hHf.js new file mode 100755 index 000000000..9383c478d --- /dev/null +++ b/app/src/main/assets/web/vue/assets/index-Wr40-hHf.js @@ -0,0 +1,10 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./BookShelf-DIQtBULC.js","./vendor-KSDcS24u.js","./vendor-CXe1BRiH.css","./loading-C4J6hIxs.js","./loading-DkQYEuap.css","./BookShelf-00b2QCsd.css","./BookChapter-Cs3stH93.js","./BookChapter-BsiFtdIw.css"])))=>i.map(i=>d[i]); +import{r as Ke,o as g,c as C,a as le,b as ae,d as K,e as E,F as N,f as h,E as We,u as i,l as V,w as p,g as y,h as f,i as Ce,j as He,k as Me,m as ce,t as Ue,n as k,p as me,s as Se,q as Fe,v as Be,x as qe,y as Ee,z as ee,A as ue,B as xe,C as A,D as te,G as Le,H as M,I as ze,J as Ge,K as fe,L as Ye,V as Qe,M as J,N as de,O as Xe,P as I,Q as Ze,R as W,S as Re,T as Ie,U as et,W as tt,X as ot,Y as z,Z as nt,_ as rt,$ as st,a0 as it,a1 as lt,a2 as at,a3 as ct}from"./vendor-KSDcS24u.js";(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const s of r)if(s.type==="childList")for(const c of s.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&n(c)}).observe(document,{childList:!0,subtree:!0});function t(r){const s={};return r.integrity&&(s.integrity=r.integrity),r.referrerPolicy&&(s.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?s.credentials="include":r.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function n(r){if(r.ep)return;r.ep=!0;const s=t(r);fetch(r.href,s)}})();const j=(e,o)=>{const t=e.__vccOpts||e;for(const[n,r]of o)t[n]=r;return t},ut={};function dt(e,o){const t=Ke("router-view");return g(),C(t)}const Te=j(ut,[["render",dt]]),pt="modulepreload",gt=function(e,o){return new URL(e,o).href},ye={},be=function(o,t,n){let r=Promise.resolve();if(t&&t.length>0){const c=document.getElementsByTagName("link"),a=document.querySelector("meta[property=csp-nonce]"),_=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));r=Promise.allSettled(t.map(w=>{if(w=gt(w,n),w in ye)return;ye[w]=!0;const U=w.endsWith(".css"),x=U?'[rel="stylesheet"]':"";if(!!n)for(let b=c.length-1;b>=0;b--){const v=c[b];if(v.href===w&&(!U||v.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${w}"]${x}`))return;const l=document.createElement("link");if(l.rel=U?"stylesheet":pt,U||(l.as="script"),l.crossOrigin="",l.href=w,_&&l.setAttribute("nonce",_),document.head.appendChild(l),U)return new Promise((b,v)=>{l.addEventListener("load",b),l.addEventListener("error",()=>v(new Error(`Unable to preload CSS for ${w}`)))})}))}function s(c){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=c,window.dispatchEvent(a),!a.defaultPrevented)throw c}return r.then(c=>{for(const a of c||[])a.status==="rejected"&&s(a.reason);return o().catch(s)})},Ve=[{path:"/",name:"shelf",component:()=>be(()=>import("./BookShelf-DIQtBULC.js"),__vite__mapDeps([0,1,2,3,4,5]),import.meta.url)},{path:"/chapter",name:"chapter",component:()=>be(()=>import("./BookChapter-Cs3stH93.js"),__vite__mapDeps([6,1,2,3,4,7]),import.meta.url)}];le({history:ae(),routes:Ve});const ht={style:{"margin-top":"20px"}},mt=K({__name:"SourceHelp",setup(e){return(o,t)=>{const n=We,r=Ce;return g(),E(N,null,[h(n,{icon:i(V),href:"/help/#appHelp",target:"_blank"},{default:p(()=>[...t[0]||(t[0]=[y("APP帮助文档",-1)])]),_:1},8,["icon"]),t[19]||(t[19]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#ruleHelp",target:"_blank"},{default:p(()=>[...t[1]||(t[1]=[y("书源制作教程",-1)])]),_:1},8,["icon"]),t[20]||(t[20]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#jsHelp",target:"_blank"},{default:p(()=>[...t[2]||(t[2]=[y("js变量和函数",-1)])]),_:1},8,["icon"]),t[21]||(t[21]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#xpathHelp",target:"_blank"},{default:p(()=>[...t[3]||(t[3]=[y("xpath语法教程",-1)])]),_:1},8,["icon"]),t[22]||(t[22]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#regexHelp",target:"_blank"},{default:p(()=>[...t[4]||(t[4]=[y("正则表达式教程",-1)])]),_:1},8,["icon"]),t[23]||(t[23]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#txtTocRuleHelp",target:"_blank"},{default:p(()=>[...t[5]||(t[5]=[y("txt目录正则说明",-1)])]),_:1},8,["icon"]),t[24]||(t[24]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#debugHelp",target:"_blank"},{default:p(()=>[...t[6]||(t[6]=[y("书源调试说明",-1)])]),_:1},8,["icon"]),t[25]||(t[25]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#httpTTSHelp",target:"_blank"},{default:p(()=>[...t[7]||(t[7]=[y("在线朗读规则",-1)])]),_:1},8,["icon"]),t[26]||(t[26]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#webDavBookHelp",target:"_blank"},{default:p(()=>[...t[8]||(t[8]=[y(" WebDav书籍简明使用教程",-1)])]),_:1},8,["icon"]),t[27]||(t[27]=f("br",null,null,-1)),h(n,{icon:i(V),href:"/help/#webDavHelp",target:"_blank"},{default:p(()=>[...t[9]||(t[9]=[y(" WebDav备份教程",-1)])]),_:1},8,["icon"]),t[28]||(t[28]=f("br",null,null,-1)),h(n,{icon:i(V),href:"https://regexr-cn.com/",target:"_blank"},{default:p(()=>[...t[10]||(t[10]=[y("正则表达式在线验证工具",-1)])]),_:1},8,["icon"]),t[29]||(t[29]=f("br",null,null,-1)),f("div",ht,[f("span",null,[h(r,null,{default:p(()=>[...t[11]||(t[11]=[f("code",null,"^$()[]{}.?+*|",-1),y(" 这些是Java正则特殊符号,匹配需转义",-1)])]),_:1})]),t[15]||(t[15]=f("br",null,null,-1)),f("span",null,[h(r,null,{default:p(()=>[...t[12]||(t[12]=[f("code",null,"(?s)",-1),y(" 前缀表示跨行解析",-1)])]),_:1})]),t[16]||(t[16]=f("br",null,null,-1)),f("span",null,[h(r,null,{default:p(()=>[...t[13]||(t[13]=[f("code",null,"(?m)",-1),y(" 前缀表示逐行匹配",-1)])]),_:1})]),t[17]||(t[17]=f("br",null,null,-1)),f("span",null,[h(r,null,{default:p(()=>[...t[14]||(t[14]=[f("code",null,"(?i)",-1),y(" 前缀表示忽略大小写",-1)])]),_:1})]),t[18]||(t[18]=f("br",null,null,-1))])],64)}}}),St=j(mt,[["__scopeId","data-v-085627fb"]]),ft="remoteUrl",yt=1e3,B=He.create({baseURL:localStorage.getItem(ft)||location.origin,timeout:120*yt});let $="",pe="",ge=()=>{},H=()=>{};const bt=e=>H=e,_t=e=>{ge=e},kt=(e,o)=>{$=new URL(e).toString(),pe=new URL(o).toString(),B.defaults.baseURL=$},vt=async(e=$)=>{const{data:o}=await B.get("getReadConfig",{baseURL:e.toString(),timeout:3e3});if(o.isSuccess)try{return JSON.parse(o.data)}catch{}},wt=e=>B.post("saveReadConfig",e),Ct=e=>B.post("saveBookProgress",e),Ut=e=>{e&&navigator.sendBeacon(new URL("saveBookProgress",$),JSON.stringify(e))},Bt=()=>B.get("getBookshelf"),Et=e=>B.get("getChapterList?url="+encodeURIComponent(e)),xt=(e,o)=>B.get("getBookContent?url="+encodeURIComponent(e)+"&index="+o),Lt=(e,o,t)=>{const n=new WebSocket(new URL("searchBook",pe));n.onerror=ge,n.onopen=()=>{n.send(`{"key":"${e}"}`)},n.onmessage=r=>{try{o(JSON.parse(r.data)),H==null||H.call(n,r)}catch{t()}},n.onclose=()=>{t()}},Rt=e=>B.post("saveBook",e),It=e=>B.post("deleteBook",e),Z=/bookSource/i.test(location.href),Tt=()=>Z?B.get("getBookSources"):B.get("getRssSources"),Vt=e=>Z?B.post("saveBookSource",e):B.post("saveRssSource",e),Nt=e=>Z?B.post("saveBookSources",e):B.post("saveRssSources",e),Ot=e=>Z?B.post("deleteBookSources",e):B.post("deleteRssSources",e),Dt=(e,o,t,n)=>{const r=new URL(`${Z?"bookSource":"rssSource"}Debug`,pe),s=new WebSocket(r);s.onerror=ge,s.onopen=()=>{s.send(JSON.stringify({tag:e,key:o}))},s.onmessage=c=>{t(c.data),H==null||H.call(s,c)},s.onclose=()=>{n()}},Jt=e=>e.startsWith($)?e:new URL("cover?path="+encodeURIComponent(e),$).toString(),Pt=(e,o,t)=>o.startsWith($)?o:new URL("image?path="+encodeURIComponent(o)+"&url="+encodeURIComponent(e)+"&width="+t,$).toString(),P={getReadConfig:vt,saveReadConfig:wt,saveBookProgress:Ct,saveBookProgressWithBeacon:Ut,getBookShelf:Bt,getChapterList:Et,getBookContent:xt,search:Lt,saveBook:Rt,deleteBook:It,getSources:Tt,saveSources:Nt,saveSource:Vt,deleteSource:Ot,debug:Dt,getProxyCoverUrl:Jt,getProxyImageUrl:Pt},X=e=>e==null||e.length===0||/^\s+$/.test(e),Uo=e=>/,\s*\{/.test(e)||!(e.startsWith("http")||e.startsWith("data:")||e.startsWith("blob:")),$t=(e,o=["https:","http:"])=>{try{const t=new URL(e),{protocol:n}=t;if(!o.includes(n))throw new Error(`Expected protocol ${o.join("/")}, but ${n}`);return!0}catch{return!1}},Bo=e=>{const o=new Date().getTime(),t=Math.floor((o-e)/1e3);let n="";return t<=30?n="刚刚":t<60?n=t+"秒前":t<3600?n=Math.floor(t/60)+"分钟前":t<86400?n=Math.floor(t/3600)+"小时前":t<2592e3?n=Math.floor(t/86400)+"天前":n=Me(new Date(e),"YYYY-MM-DD"),n},jt={theme:0,font:0,fontSize:18,readWidth:800,infiniteLoading:!1,customFontName:"",jumpDuration:1e3,spacing:{paragraph:1,line:.8,letter:0}};let _e;const At=ce("book",{state:()=>({searchBooks:[],shelf:[],catalog:[],readingBook:{chapterPos:0,chapterIndex:0},popCataVisible:!1,contentLoading:!0,showContent:!1,config:jt,miniInterface:!1,readSettingsVisible:!1}),getters:{bookProgress:e=>{var c;if(e.catalog.length==0)return;const{chapterIndex:o,chapterPos:t,name:n,author:r}=e.readingBook,s=(c=e.catalog[o])==null?void 0:c.title;if(s)return{name:n,author:r,durChapterIndex:o,durChapterPos:t,durChapterTime:new Date().getTime(),durChapterTitle:s}},theme:e=>e.config.theme,isNight:e=>e.config.theme==6},actions:{async loadBookShelf(){const e=P.getBookShelf().then(o=>{const{isSuccess:t,data:n,errorMsg:r}=o.data;if(t===!0)this.shelf.length!==n.length&&this.shelf.length>0&&n.length>0&&k.info("书架数据已更新"),this.shelf=n.sort((s,c)=>{const a=s.durChapterTime||0;return(c.durChapterTime||0)-a});else{if(r.includes("还没有添加小说")&&this.shelf.length>0)return k.info("当前书架上的书籍已经被删除"),this.shelf=[];k.error(r??"后端返回格式错误!")}return this.shelf});return this.shelf.length>0?this.shelf:await e},async loadWebCatalog(e){const{bookUrl:o,name:t,chapterIndex:n}=e,r=P.getChapterList(o).then(s=>{const{isSuccess:c,data:a,errorMsg:_}=s.data;if(c===!1)throw k.error(_),new Error;return o===this.readingBook.bookUrl&&a.length!==this.catalog.length&&a.length>0&&this.catalog.length>0&&k.info(`书籍${t}: 章节目录已更新`),this.catalog=a,this.catalog});return o===this.readingBook.bookUrl&&this.catalog.length>0&&this.catalog.length-1>=n?this.catalog:await r},setPopCataVisible(e){this.popCataVisible=e},setContentLoading(e){this.contentLoading=e},setReadingBook(e){this.readingBook=e},async loadWebConfig(){if(_e===void 0){const e=await P.getReadConfig();return _e=new Date,this.setConfig(e)}},setConfig(e){this.config=Object.assign({},this.config,e)},setReadSettingsVisible(e){this.readSettingsVisible=e},setShowContent(e){this.showContent=e},setMiniInterface(e){this.miniInterface=e},async setSearchBooks(e){e.forEach(o=>{this.shelf.every(n=>n.bookUrl!==o.bookUrl)===!0&&this.searchBooks.push(o)})},clearSearchBooks(){this.searchBooks=[]},async saveBookProgress(){if(!this.bookProgress)return Promise.resolve();const{bookUrl:e}=this.readingBook,o=Ue(this.shelf),t=o.findIndex(n=>n.bookUrl===e);return t>-1&&(this.shelf[t]=Object.assign({},o[t],this.bookProgress)),P.saveBookProgressWithBeacon(this.bookProgress)}}}),oe=e=>"bookSourceName"in e,Kt=e=>oe(e)?!X(e.bookSourceName)&&!X(e.bookSourceUrl)&&!X(e.bookSourceType):!X(e.sourceName)&&!X(e.sourceUrl),ne=e=>oe(e)?e.bookSourceUrl:e.sourceUrl,he=e=>oe(e)?e.bookSourceName:e.sourceName,Wt=(e,o)=>{var t,n,r,s;return oe(e)?(e.bookSourceName.includes(o)||e.bookSourceUrl.includes(o)||((t=e.bookSourceGroup)==null?void 0:t.includes(o))||((n=e.bookSourceComment)==null?void 0:n.includes(o)))??!1:(e.sourceName.includes(o)||e.sourceUrl.includes(o)||((r=e.sourceGroup)==null?void 0:r.includes(o))||((s=e.sourceComment)==null?void 0:s.includes(o)))??!1},ie=e=>{const o=new Map;return e.forEach(t=>o.set(ne(t),t)),o},Ne=e=>{for(const o in e){const t=e[o];t===""||t===null||typeof t=="string"&&!t.trim()?delete e[o]:t instanceof Object&&Ne(t)}},Ht={ruleSearch:{},ruleBookInfo:{},ruleToc:{},ruleContent:{},ruleExplore:{}},Mt={},G=/bookSource/i.test(location.href),ke=G?Ht:Mt,F=ce("source",{state:()=>({bookSources:Se([]),rssSources:Se([]),savedSources:[],currentSource:JSON.parse(JSON.stringify(ke)),currentTab:localStorage.getItem("tabName")||"editTab",editTabSource:{},isDebuging:!1}),getters:{sources:e=>G?e.bookSources:e.rssSources,sourcesMap:function(){return ie(this.sources)},savedSourcesMap:e=>ie(e.savedSources),currentSourceUrl:e=>G?e.currentSource.bookSourceUrl:e.currentSource.sourceUrl,searchKey:e=>{var o,t;return G?((t=(o=e.currentSource)==null?void 0:o.ruleSearch)==null?void 0:t.checkKeyWord)||"我的":""}},actions:{startDebug(){this.currentTab="editDebug",this.isDebuging=!0},debugFinish(){this.isDebuging=!1},saveSources(e){G?this.bookSources=me(e):this.rssSources=me(e)},setPushReturnSources(e){this.savedSources=e},deleteSources(e){const o=G?this.bookSources:this.rssSources;e.forEach(t=>{const n=o.indexOf(t);n>-1&&o.splice(n,1)})},saveCurrentSource(){const e=this.currentSource,o=this.sourcesMap;o.set(ne(e),JSON.parse(JSON.stringify(e))),this.saveSources(Array.from(o.values()))},changeCurrentSource(e){this.currentSource=JSON.parse(JSON.stringify(e))},changeTabName(e){this.currentTab=e,localStorage.setItem("tabName",e)},changeEditTabSource(e){this.editTabSource=JSON.parse(JSON.stringify(e))},editHistory(e){let o;if(localStorage.getItem("history"))o=JSON.parse(localStorage.getItem("history")),o.new.push(e),o.new.length>50&&o.new.shift(),o.old.length>50&&o.old.shift(),localStorage.setItem("history",JSON.stringify(o));else{const t={new:[e],old:[]};localStorage.setItem("history",JSON.stringify(t))}},editHistoryUndo(){if(localStorage.getItem("history")){const e=JSON.parse(localStorage.getItem("history"));e.old.push(this.currentSource),e.new.length&&(this.currentSource=e.new.pop()),localStorage.setItem("history",JSON.stringify(e))}},clearAllHistory(){localStorage.setItem("history",JSON.stringify({new:[],old:[]}))},clearEdit(){this.editTabSource={},this.currentSource=JSON.parse(JSON.stringify(ke))},clearAllSource(){this.bookSources=[],this.rssSources=[],this.savedSources=[]}}}),Ft=ce("connection",{state:()=>({connectStatus:"正在连接后端服务器……",connectType:"primary",newConnect:!1}),actions:{setConnectStatus(e){this.newConnect!==!0&&(this.connectStatus=e)},setConnectType(e){this.newConnect!==!0&&(this.connectType=e)},setNewConnect(e){this.newConnect=e}}}),Oe=Fe();Be(Te).use(Oe);const Y=Ft(),ve=Array.of("isSuccess","errorMsg"),De=k,qt=e=>{let o=!0;try{const t=e.data;for(const n of ve)n in t||(o=!1,ve.length=0);t.isSuccess===!0&&("data"in t||(o=!1))}catch{o=!1}if(o===!1)throw De.warning({message:"后端返回内容格式错误",grouping:!0}),new Error;return Y.setConnectType("primary"),Y.setConnectStatus("已连接 "+$),e},Je=e=>{throw De.error({message:"后端连接失败,请检查阅读WEB服务或者设置其它可用链接",grouping:!0}),Y.setConnectType("danger"),Y.setConnectStatus("连接异常"),e};B.interceptors.response.use(qt,Je);_t(Je);bt(()=>{Y.setConnectType("primary"),Y.setConnectStatus("已连接 "+$)});const zt=e=>{let o=new URL(location.origin);$t(e)&&(o=new URL(e));const{protocol:t,port:n}=o;let r;n!==""?r=String(Number(n)+1):r=t.startsWith("https:")?"444":"81";const s=t.startsWith("https:")?"wss://":"ws://",c=o.toString();o.protocol=s,o.port=r;const a=o.toString();return[c,a]};kt(...zt(B.defaults.baseURL));const Gt=K({__name:"SourceItem",props:{source:{}},setup(e){const o=e,t=F(),n=A(()=>t.currentSourceUrl),r=A(()=>ne(o.source)),s=a=>{t.changeCurrentSource(a)},c=A(()=>{const a=t.savedSourcesMap;return a.size==0?!1:!a.has(r.value)});return(a,_)=>{const w=ue,U=qe;return g(),C(U,{size:"large",border:"",value:i(r),class:Ee({error:i(c),edit:i(r)==i(n)})},{default:p(()=>[y(ee(i(he)(e.source))+" ",1),h(w,{text:"",icon:i(xe),onClick:_[0]||(_[0]=x=>s(e.source))},null,8,["icon"])]),_:1},8,["value","class"])}}}),Yt=j(Gt,[["__scopeId","data-v-d8dae8d3"]]),Qt={class:"tool"},Xt=K({__name:"SourceList",setup(e){const o=F(),t=J([]),n=J(""),r=A(()=>o.sources),s=A(()=>{const d=n.value;return d===""?r.value:r.value.filter(l=>Wt(l,d))}),c=A(()=>{const d=t.value;if(d.length==0)return[];const l=n.value==""?o.sourcesMap:ie(s.value);return d.reduce((b,v)=>{const L=l.get(v);return L&&b.push(L),b},[])}),a=()=>{const d=c.value;P.deleteSource(d).then(({data:l})=>{if(!l.isSuccess)return k.error(l.errorMsg);o.deleteSources(d);const b=Ue(t.value);d.forEach(v=>{const L=b.indexOf(ne(v));L>-1&&b.splice(L,1)}),t.value=b})},_=()=>{o.clearAllSource(),t.value=[]},w=()=>{const d=document.createElement("input");d.type="file",d.accept=".json,.txt",d.addEventListener("change",()=>{const l=d.files;if(l===null)return k.info("未选择文件");const b=new FileReader;b.readAsText(l[0]),b.onload=()=>{try{const v=JSON.parse(b.result);o.saveSources(v)}catch(v){k.error("上传的源格式错误: "+v.message)}}}),d.click()},U=/bookSource/i.test(window.location.href),x=()=>{const d=document.createElement("a"),l=t.value.length===0?s.value:c.value,b=U?"BookSource":"RssSource";d.download=`${b}_${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/,"$2$1$3").replace(/:/g,"")}.json`;const v=new Blob([JSON.stringify(l,null,4)],{type:"application/json"});d.href=window.URL.createObjectURL(v),d.click(),window.URL.revokeObjectURL(d.href)};return(d,l)=>{const b=te,v=ue,L=Ye;return g(),E(N,null,[h(b,{modelValue:i(n),"onUpdate:modelValue":l[0]||(l[0]=O=>M(n)?n.value=O:null),class:"search","prefix-icon":i(Le),placeholder:"筛选源"},null,8,["modelValue","prefix-icon"]),f("div",Qt,[h(v,{onClick:w,icon:i(ze)},{default:p(()=>[...l[2]||(l[2]=[y("打开",-1)])]),_:1},8,["icon"]),h(v,{disabled:i(s).length===0,onClick:x,icon:i(Ge)},{default:p(()=>[...l[3]||(l[3]=[y(" 导出",-1)])]),_:1},8,["disabled","icon"]),h(v,{type:"danger",icon:i(fe),onClick:a,disabled:i(c).length===0},{default:p(()=>[...l[4]||(l[4]=[y("删除",-1)])]),_:1},8,["icon","disabled"]),h(v,{type:"danger",icon:i(fe),onClick:_,disabled:i(r).length===0},{default:p(()=>[...l[5]||(l[5]=[y("清空",-1)])]),_:1},8,["icon","disabled"])]),h(L,{id:"source-list",modelValue:i(t),"onUpdate:modelValue":l[1]||(l[1]=O=>M(t)?t.value=O:null)},{default:p(()=>[h(i(Qe),{style:{height:"100%","overflow-y":"auto","overflow-x":"hidden"},"data-key":O=>i(he)(O),"data-sources":i(s),"data-component":Yt,"estimate-size":45},null,8,["data-key","data-sources"])]),_:1},8,["modelValue"])],64)}}}),Zt=j(Xt,[["__scopeId","data-v-258cd99b"]]),eo=K({__name:"SourceDebug",setup(e){const o=F(),t=J(""),n=J("");de(()=>o.isDebuging,()=>{o.isDebuging&&s()});const r=a=>{const _=document.querySelector("#debug-text");_.scrollTop=_.scrollHeight,t.value+=a+` +`},s=async()=>{t.value="";try{await P.saveSource(o.currentSource)}catch(a){throw o.debugFinish(),a}P.debug(o.currentSourceUrl,n.value||o.searchKey,r,o.debugFinish)},c=A(()=>/bookSource/i.test(window.location.href));return(a,_)=>{const w=te;return g(),E(N,null,[i(c)?(g(),C(w,{key:0,id:"debug-key",modelValue:i(n),"onUpdate:modelValue":_[0]||(_[0]=U=>M(n)?n.value=U:null),placeholder:"搜索书名、作者","prefix-icon":i(Le),style:{"padding-bottom":"4px"},onKeydown:Xe(s,["enter"])},null,8,["modelValue","prefix-icon"])):I("",!0),h(w,{id:"debug-text",modelValue:i(t),"onUpdate:modelValue":_[1]||(_[1]=U=>M(t)?t.value=U:null),type:"textarea",readonly:"",rows:29,placeholder:"这里用于输出调试信息"},null,8,["modelValue"])],64)}}}),to=j(eo,[["__scopeId","data-v-3ac68c8a"]]),oo=K({__name:"SourceJson",setup(e){const o=F(),t=J(""),n=async r=>{try{o.changeEditTabSource(JSON.parse(r))}catch{k({message:"粘贴的源格式错误",type:"error"})}};return Ze(async()=>{const r=o.editTabSource;Object.keys(r).length>0?t.value=JSON.stringify(r,null,4):t.value=""}),(r,s)=>{const c=te;return g(),C(c,{id:"source-json",modelValue:i(t),"onUpdate:modelValue":s[0]||(s[0]=a=>M(t)?t.value=a:null),type:"textarea",placeholder:"这里输出序列化的JSON数据,可直接导入'阅读'APP",rows:30,onChange:n,style:{"margin-bottom":"4px"}},null,8,["modelValue"])}}}),no=j(oo,[["__scopeId","data-v-f62d9369"]]),ro=K({__name:"SourceTabTools",setup(e){const o=F(),t=A({get:()=>o.currentTab,set:r=>o.currentTab=r}),n=J([["editTab","编辑源"],["editDebug","调试源"],["editList","源列表"],["editHelp","帮助信息"]]);return(r,s)=>{const c=no,a=to,_=Zt,w=St,U=Re,x=Ie;return g(),C(x,{modelValue:i(t),"onUpdate:modelValue":s[0]||(s[0]=d=>M(t)?t.value=d:null)},{default:p(()=>[(g(!0),E(N,null,W(i(n),(d,l)=>(g(),C(U,{key:d[0],name:d[0],label:d[1]},{default:p(()=>[l==0?(g(),C(c,{key:0})):I("",!0),l==1?(g(),C(a,{key:1})):I("",!0),l==2?(g(),C(_,{key:2})):I("",!0),l==3?(g(),C(w,{key:3})):I("",!0)]),_:2},1032,["name","label"]))),128))]),_:1},8,["modelValue"])}}}),so=j(ro,[["__scopeId","data-v-fd81540f"]]),io={class:"menu flex-column-center"},lo={class:"hotkeys-header flex-space-between"},ao=["id"],co={key:0},uo={class:"hotkeys-settings flex-column-center"},po={class:"title"},go={class:"hotkeys-item__content"},ho={key:0},mo={key:0},So=K({__name:"ToolBar",setup(e){const o=F(),t=()=>{const S=k({message:"加载中……",showClose:!0,duration:0});P.getSources().then(({data:u})=>{u.isSuccess?(o.changeTabName("editList"),o.saveSources(u.data),k({message:`成功拉取${u.data.length}条源`,type:"success"})):k({message:u.errorMsg??"后端错误",type:"error"})}).finally(()=>S.close())},n=()=>{const S=o.sources;if(o.changeTabName("editList"),S.length===0)return k({message:"空空如也",type:"info"});k({message:"正在推送中",type:"info"}),P.saveSources(S).then(({data:u})=>{if(u.isSuccess){const m=u.data;if(Array.isArray(m)){let D="";S.length>m.length&&(D=` +推送失败的源将用红色字体标注!`,o.setPushReturnSources(m)),k({message:`批量推送源到「阅读3.0APP」 +共计: ${S.length} 条 +成功: ${m.length} 条 +失败: ${S.length-m.length} 条${D}`,type:"success"})}}else k({message:`批量推送源失败! +ErrorMsg: ${u.errorMsg}`,type:"error"})})},r=()=>{o.changeTabName("editTab"),o.changeEditTabSource(o.currentSource)},s=()=>{o.changeCurrentSource(o.editTabSource)},c=()=>{o.editHistoryUndo()},a=()=>{o.clearEdit(),k({message:"已清除",type:"success"})},_=()=>{o.clearEdit(),o.clearAllHistory(),k({message:"已清除所有历史记录",type:"success"})},w=()=>{const S=o.currentSource;Kt(S)?(Ne(S),P.saveSource(S).then(({data:u})=>{const m=he(S);u.isSuccess?(k({message:`源《${m}》已成功保存到「阅读3.0APP」`,type:"success"}),o.saveCurrentSource()):k({message:`源《${m}》保存失败! +ErrorMsg: ${u.errorMsg}`,type:"error"})})):k({message:"请检查<必填>项是否全部填写",type:"error"})},U=()=>{o.startDebug()},x=J(Array.of({name:"⇈推送源",hotKeys:[],action:n},{name:"⇊拉取源",hotKeys:[],action:t},{name:"⋙生成源",hotKeys:[],action:r},{name:"⋘编辑源",hotKeys:[],action:s},{name:"✗清空表单",hotKeys:[],action:a},{name:"↶撤销操作",hotKeys:[],action:c},{name:"↷重做操作",hotKeys:[],action:_},{name:"⇏调试源",hotKeys:[],action:U},{name:"✓保存源",hotKeys:[],action:w})),d=J(!0),l=J(!1),b=J(-1),v=()=>{l.value||(d.value=!1),l.value=!1};de(d,S=>{if(!S){z.unbind("*"),Q(),q();return}Q(),z.unbind(),z("*",u=>{u.preventDefault();const m=z.getPressedKeyString();m.length==1&&m[0]=="esc"||l.value&&b.value>-1&&(x.value[b.value].hotKeys=m)})},{immediate:!0});const L=S=>{l.value=!0,k({message:"按ESC键或者点击空白处结束录入",type:"info"}),x.value[S].hotKeys=[],b.value=S},O=()=>{const S=[];x.value.forEach(({hotKeys:u})=>{S.push(u)}),T(S),d.value=!1},q=()=>{z.filter=()=>!0,x.value.forEach(({hotKeys:S,action:u})=>{S.length!=0&&z(S.join("+"),m=>{m.preventDefault(),u.call(null)})})},T=S=>{localStorage.setItem("legado_web_hotkeys",JSON.stringify(S))};function Q(){try{const S=localStorage.getItem("legado_web_hotkeys");if(S===null)return!1;const u=JSON.parse(S);return!Array.isArray(u)||u.length==0?!1:(x.value.forEach((m,D)=>m.hotKeys=u[D]),!0)}catch{k({message:"快捷键配置错误",type:"error"}),localStorage.removeItem("legado_web_hotkeys")}return!1}return et(()=>{Q()&&(d.value=!1)}),(S,u)=>{const m=ue,D=Ce,je=tt;return g(),E(N,null,[f("div",io,[(g(!0),E(N,null,W(i(x),R=>(g(),C(m,{size:"large",key:R.name,onClick:R.action},{default:p(()=>[y(ee(R.name),1)]),_:2},1032,["onClick"]))),128)),h(m,{size:"large",onClick:u[0]||(u[0]=()=>d.value=!0)},{default:p(()=>[...u[2]||(u[2]=[y("快捷键",-1)])]),_:1})]),h(je,{modelValue:i(d),"onUpdate:modelValue":u[1]||(u[1]=R=>M(d)?d.value=R:null),"show-close":!1,"before-close":v},{header:p(({titleClass:R,titleId:re})=>[f("div",lo,[f("div",{id:re,class:Ee(R)},[u[4]||(u[4]=y(" 快捷键设置 ",-1)),i(l)?(g(),E("span",co,[h(D,null,{default:p(()=>[...u[3]||(u[3]=[y(" / 录入中 ",-1)])]),_:1})])):I("",!0)],10,ao),h(m,{disabled:i(l),onClick:O,icon:i(ot)},{default:p(()=>[...u[5]||(u[5]=[y("保存",-1)])]),_:1},8,["disabled","icon"])])]),default:p(()=>[f("div",uo,[(g(!0),E(N,null,W(i(x),(R,re)=>(g(),E("div",{key:R.name,class:"hotkeys-item flex-space-between"},[f("span",po,[h(D,null,{default:p(()=>[y(ee(R.name),1)]),_:2},1024)]),f("div",go,[(g(!0),E(N,null,W(R.hotKeys,(se,Ae)=>(g(),E("div",{key:se},[f("kbd",null,ee(se),1),Ae+1[...u[6]||(u[6]=[y("+",-1)])]),_:1})])):I("",!0)]))),128)),R.hotKeys.length==0?(g(),E("span",mo,"未设置")):I("",!0)]),h(m,{disabled:i(l),text:"",icon:i(xe),onClick:se=>L(re)},{default:p(()=>[...u[7]||(u[7]=[y("编辑",-1)])]),_:1},8,["disabled","icon","onClick"])]))),128))])]),_:1},8,["modelValue"])],64)}}}),fo=j(So,[["__scopeId","data-v-096adf6d"]]),yo=K({__name:"SourceTabForm",props:{config:{}},setup(e){const o=F(),t=A(()=>o.currentSource);return(n,r)=>{const s=te,c=st,a=it,_=at,w=lt,U=rt,x=nt,d=Re,l=Ie;return g(),C(l,{id:"source-edit"},{default:p(()=>[(g(!0),E(N,null,W(Object.values(e.config),({name:b,children:v})=>(g(),C(d,{label:b,key:b},{default:p(()=>[h(x,{"label-position":"right","label-width":"auto"},{default:p(()=>[(g(!0),E(N,null,W(v,({type:L,title:O,namespace:q,id:T,array:Q,hint:S,required:u=!1})=>(g(),C(U,{label:O,key:O,required:u},{default:p(()=>[L=="String"&&typeof q>"u"?(g(),C(s,{key:0,type:"textarea",modelValue:i(t)[T],"onUpdate:modelValue":m=>i(t)[T]=m,placeholder:S,autosize:""},null,8,["modelValue","onUpdate:modelValue","placeholder"])):I("",!0),L=="String"&&typeof q<"u"?(g(),C(s,{key:1,type:"textarea",modelValue:i(t)[q][T],"onUpdate:modelValue":m=>i(t)[q][T]=m,placeholder:S,autosize:""},null,8,["modelValue","onUpdate:modelValue","placeholder"])):I("",!0),L==="Boolean"?(g(),C(c,{key:2,modelValue:i(t)[T],"onUpdate:modelValue":m=>i(t)[T]=m},null,8,["modelValue","onUpdate:modelValue"])):I("",!0),L==="Number"?(g(),C(a,{key:3,modelValue:i(t)[T],"onUpdate:modelValue":m=>i(t)[T]=m,min:0},null,8,["modelValue","onUpdate:modelValue"])):I("",!0),L==="Array"?(g(),C(w,{key:4,modelValue:i(t)[T],"onUpdate:modelValue":m=>i(t)[T]=m},{default:p(()=>[(g(!0),E(N,null,W(Q,(m,D)=>(g(),C(_,{value:D,key:m,label:m},null,8,["value","label"]))),128))]),_:2},1032,["modelValue","onUpdate:modelValue"])):I("",!0)]),_:2},1032,["label","required"]))),128))]),_:2},1024)]),_:2},1032,["label"]))),128))]),_:1})}}}),bo=j(yo,[["__scopeId","data-v-c07c5146"]]),_o={base:{name:"基础",children:[{title:"源类型",id:"bookSourceType",type:"Array",array:["文本","音频","图片","文件"],required:!0},{title:"源域名",id:"bookSourceUrl",type:"String",hint:"通常填写网站主页,例: https://www.qidian.com",required:!0},{title:"源名称",id:"bookSourceName",type:"String",hint:"会显示在源列表",required:!0},{title:"源分组",id:"bookSourceGroup",type:"String",hint:"描述源的特征信息"},{title:"源注释",id:"bookSourceComment",type:"String",hint:"描述源作者和状态"},{title:"登录地址",id:"loginUrl",type:"String",hint:"填写网站登录网址,仅在需要登录的源有用"},{title:"登录界面",id:"loginUi",type:"String",hint:"自定义登录界面"},{title:"登录检测",id:"loginCheckJs",type:"String",hint:"登录检测js"},{title:"封面解密",id:"coverDecodeJs",type:"String",hint:"封面解密js"},{title:"链接验证",id:"bookUrlPattern",type:"String",hint:"书籍URL正则,当详情页URL与源URL的域名不一致时有效,用于添加网址"},{title:"请求头",id:"header",type:"String",hint:"客户端标识"},{title:"变量说明",id:"variableComment",type:"String",hint:"书源变量说明"},{title:"并发率",id:"concurrentRate",type:"String",hint:"并发率,如1000(访问间隔1000ms)或者1/1000(1000ms内访问1次)"},{title:"js库",id:"jsLib",type:"String",hint:"js库, 可填写js或者key-value object获取在线js文件"}]},search:{name:"搜索",children:[{title:"搜索地址",id:"searchUrl",type:"String",hint:"[域名可省略]/search.php@kw={{key}}"},{title:"校验文字",namespace:"ruleSearch",id:"checkKeyWord",type:"String",hint:"校验关键字,强烈建议填写"},{title:"列表规则",namespace:"ruleSearch",id:"bookList",type:"String",hint:"选择书籍节点 (规则结果为List)"},{title:"书名规则",namespace:"ruleSearch",id:"name",type:"String",hint:"选择节点书名 (规则结果为String)"},{title:"作者规则",namespace:"ruleSearch",id:"author",type:"String",hint:"选择节点作者 (规则结果为String)"},{title:"分类规则",namespace:"ruleSearch",id:"kind",type:"String",hint:"选择节点分类信息 (规则结果为String)"},{title:"字数规则",namespace:"ruleSearch",id:"wordCount",type:"String",hint:"选择节点字数信息 (规则结果为String)"},{title:"最新章节",namespace:"ruleSearch",id:"lastChapter",type:"String",hint:"选择节点最新章节 (规则结果为String)"},{title:"简介规则",namespace:"ruleSearch",id:"intro",type:"String",hint:"选择节点书籍简介 (规则结果为String)"},{title:"封面规则",namespace:"ruleSearch",id:"coverUrl",type:"String",hint:"选择节点书籍封面 (规则结果为String类型的url)"},{title:"详情地址",namespace:"ruleSearch",id:"bookUrl",type:"String",hint:"选择书籍详情页网址 (规则结果为String类型的url)"}]},find:{name:"发现",children:[{title:"发现地址",id:"exploreUrl",type:"String",hint:"单个发现格式::或者{url:,title:,style:...};前者用换行符或者&&连接,后者放在数组内;可用js动态生成"},{title:"列表规则",namespace:"ruleExplore",id:"bookList",type:"String",hint:"选择书籍节点 (规则结果为List)"},{title:"书名规则",namespace:"ruleExplore",id:"name",type:"String",hint:"选择节点书名 (规则结果为String)"},{title:"作者规则",namespace:"ruleExplore",id:"author",type:"String",hint:"选择节点作者 (规则结果为String)"},{title:"分类规则",namespace:"ruleExplore",id:"kind",type:"String",hint:"选择节点分类信息 (规则结果为String)"},{title:"字数规则",namespace:"ruleExplore",id:"wordCount",type:"String",hint:"选择节点字数信息 (规则结果为String)"},{title:"最新章节",namespace:"ruleExplore",id:"lastChapter",type:"String",hint:"选择节点最新章节 (规则结果为String)"},{title:"简介规则",namespace:"ruleExplore",id:"intro",type:"String",hint:"选择节点书籍简介 (规则结果为String)"},{title:"封面规则",namespace:"ruleExplore",id:"coverUrl",type:"String",hint:"选择节点书籍封面 (规则结果为String类型的url)"},{title:"详情地址",namespace:"ruleExplore",id:"bookUrl",type:"String",hint:"选择书籍详情页网址 (规则结果为String类型的url)"}]},detail:{name:"详情",children:[{title:"预处理",namespace:"ruleBookInfo",id:"init",type:"String",hint:"用于加速详情信息检索,只支持AllInOne规则"},{title:"书名规则",namespace:"ruleBookInfo",id:"name",type:"String",hint:"选择节点书名 (规则结果为String)"},{title:"作者规则",namespace:"ruleBookInfo",id:"author",type:"String",hint:"选择节点作者 (规则结果为String)"},{title:"分类规则",namespace:"ruleBookInfo",id:"kind",type:"String",hint:"选择节点分类信息 (规则结果为String)"},{title:"字数规则",namespace:"ruleBookInfo",id:"wordCount",type:"String",hint:"选择节点字数信息 (规则结果为String)"},{title:"最新章节",namespace:"ruleBookInfo",id:"lastChapter",type:"String",hint:"选择节点最新章节 (规则结果为String)"},{title:"简介规则",namespace:"ruleBookInfo",id:"intro",type:"String",hint:"选择节点书籍简介 (规则结果为String)"},{title:"封面规则",namespace:"ruleBookInfo",id:"coverUrl",type:"String",hint:"选择节点书籍封面 (规则结果为String类型的url)"},{title:"目录地址",namespace:"ruleBookInfo",id:"tocUrl",type:"String",hint:"选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)"},{title:"修改书籍",namespace:"ruleBookInfo",id:"canReName",type:"String",hint:"允许修改书名作者(规则结果为String类型, 默认不允许)"},{title:"下载URL",namespace:"ruleBookInfo",id:"downloadUrls",type:"String",hint:"文件类书源下载地址 (规则结果为String类型的url, 多个链接返回数组)"}]},directory:{name:"目录",children:[{title:"更新前JS",namespace:"ruleToc",id:"preUpdateJs",type:"String",hint:"更新目录前调用JS 动态更新目录链接"},{title:"列表规则",namespace:"ruleToc",id:"chapterList",type:"String",hint:"选择目录列表的章节节点 (规则结果为List)"},{title:"章节名称",namespace:"ruleToc",id:"chapterName",type:"String",hint:"选择章节名称 (规则结果为String)"},{title:"章节地址",namespace:"ruleToc",id:"chapterUrl",type:"String",hint:"选择章节链接 (规则结果为String类型的Url)"},{title:"标题处理",namespace:"ruleToc",id:"formatJs",type:"String",hint:"遍历去重后的章节列表的回调,提供index(章节序号从1开始)、title(章节标题)变量,额外提供gInt(初始值0),返回值作为新的标题"},{title:"卷名标识",namespace:"ruleToc",id:"isVolume",type:"String",hint:"章节名称是否是卷名 (规则结果为Bool)"},{title:"章节信息",namespace:"ruleToc",id:"updateTime",type:"String",hint:"选择章节信息(如更新时间) (规则结果为String)"},{title:"收费标识",namespace:"ruleToc",id:"isVip",type:"String",hint:"章节是否为VIP章节 (规则结果为Bool)"},{title:"购买标识",namespace:"ruleToc",id:"isPay",type:"String",hint:"章节是否为已购买 (规则结果为Bool)"},{title:"翻页规则",namespace:"ruleToc",id:"nextTocUrl",type:"String",hint:"选择目录下一页链接 (规则结果为List或String)"}]},content:{name:"正文",children:[{title:"正文规则",namespace:"ruleContent",id:"content",type:"String",hint:"选择正文内容 (规则结果为String)"},{title:"标题规则",namespace:"ruleContent",id:"title",type:"String",hint:"获取结果将会覆盖章节标题 (规则结果为String)"},{title:"翻页规则",namespace:"ruleContent",id:"nextContentUrl",type:"String",hint:"选择下一分页(不是下一章)链接 (规则结果为String类型的Url)"},{title:"脚本注入",namespace:"ruleContent",id:"webJs",type:"String",hint:"注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"},{title:"资源正则",namespace:"ruleContent",id:"sourceRegex",type:"String",hint:"匹配资源的url特征,用于嗅探"},{title:"替换规则",namespace:"ruleContent",id:"replaceRegex",type:"String",hint:"多页内容合并后替换,用于正文净化"},{title:"图片样式",namespace:"ruleContent",id:"imageStyle",type:"String",hint:"FULL:铺满 不填:默认样式"},{title:"图片解密",namespace:"ruleContent",id:"imageDecode",type:"String",hint:"填写JavaScript 返回解密图片的bytes "},{title:"购买操作",namespace:"ruleContent",id:"payAction",type:"String",hint:"填写JavaScript 返回购买链接或者调用购买接口"}]},other:{name:"其他",children:[{title:"启用搜索",id:"enabled",type:"Boolean"},{title:"启用发现",id:"enabledExplore",type:"Boolean"},{title:"CookieJar",id:"enabledCookieJar",type:"Boolean"},{title:"搜索权重",id:"weight",type:"Number"},{title:"排序编号",id:"customOrder",type:"Number"}]}},ko={base:{name:"基础",children:[{title:"源域名",id:"sourceUrl",type:"String",hint:"通常填写网站主页,例: https://www.qidian.com",required:!0},{title:"图标",id:"sourceIcon",type:"String",hint:"填写图片网络链接"},{title:"源名称",id:"sourceName",type:"String",hint:"会显示在源列表",required:!0},{title:"源分组",id:"sourceGroup",type:"String",hint:"描述源的特征信息"},{title:"源注释",id:"sourceComment",type:"String",hint:"描述源作者和状态"},{title:"分类地址",id:"sortUrl",type:"String",hint:`名称1::链接1 +名称2::链接2`},{title:"登录地址",id:"loginUrl",type:"String",hint:"填写网站登录网址,仅在需要登录的源有用"},{title:"登录界面",id:"loginUi",type:"String",hint:"自定义登录界面"},{title:"登录检测",id:"loginCheckJs",type:"String",hint:"登录检测js"},{title:"封面解密",id:"coverDecodeJs",type:"String",hint:"封面解密js"},{title:"请求头",id:"header",type:"String",hint:"客户端标识"},{title:"变量说明",id:"variableComment",type:"String",hint:"源变量说明"},{title:"并发率",id:"concurrentRate",type:"String",hint:"并发率"},{title:"js库",id:"jsLib",type:"String",hint:"js库, 可填写js或者key-value object获取在线js文件"}]},list:{name:"列表",children:[{title:"列表规则",id:"ruleArticles",type:"String",hint:"规则结果为List"},{title:"翻页规则",id:"ruleNextPage",type:"String",hint:"下一页链接 规则结果为List或String"},{title:"标题规则",id:"ruleTitle",type:"String",hint:"文章标题 规则结果为String"},{title:"时间规则",id:"rulePubDate",type:"String",hint:"文章发布时间 规则结果为String"},{title:"描述规则",id:"ruleDescription",type:"String",hint:"文章简要描述 规则结果为String"},{title:"图片规则",id:"ruleImage",type:"String",hint:"文章图片链接 规则结果为String"},{title:"链接规则",id:"ruleLink",type:"String",hint:"文章链接 规则结果为String"}]},webView:{name:"WebView",children:[{title:"内容规则",id:"ruleContent",type:"String",hint:"文章正文"},{title:"样式规则",id:"style",type:"String",hint:"文章正文样式 填写css"},{title:"注入规则",id:"injectJs",type:"String",hint:"注入网页的JavaScript"},{title:"黑名单",id:"contentBlacklist",type:"String",hint:"webView链接加载黑名单,英文逗号隔开"},{title:"白名单",id:"contentWhitelist",type:"String",hint:"webView链接加载白名单,英文逗号隔开"},{title:"链接拦截",id:"shouldOverrideUrlLoading",type:"String",hint:"填写js,变量url为当前资源链接,返回true拦截"}]},other:{name:"其他",children:[{title:"列表样式",id:"articleStyle",type:"Array",array:["默认","大图","双列"]},{title:"加载地址",id:"loadWithBaseUrl",type:"Boolean"},{title:"启用JS",id:"enableJs",type:"Boolean"},{title:"启用",id:"enabled",type:"Boolean"},{title:"Cookie",id:"enabledCookieJar",type:"Boolean"},{title:"单URL",id:"singleUrl",type:"Boolean"},{title:"排序编号",id:"customOrder",type:"Number"}]}},vo={class:"editor"},wo=K({__name:"SourceEditor",setup(e){ct();let o;return/bookSource/i.test(location.href)?(o=_o,document.title="书源管理"):(o=ko,document.title="订阅源管理"),(t,n)=>{const r=bo,s=fo,c=so;return g(),E("div",vo,[h(r,{class:"left",config:i(o)},null,8,["config"]),h(s),h(c,{class:"right"})])}}}),we=j(wo,[["__scopeId","data-v-f2c47af3"]]),Pe=[{path:"/bookSource",name:"book-home",component:we},{path:"/rssSource",name:"rss-home",component:we}];le({history:ae(),routes:Pe});const $e=le({history:ae(),routes:[Ve,Pe].flat()});$e.afterEach(e=>{e.name=="shelf"&&(document.title="书架")});Be(Te).use(Oe).use($e).mount("#app");de(()=>At().isNight,e=>{e?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")});window.addEventListener("vite:preloadError",e=>{e.preventDefault()});export{P as A,j as _,Ft as a,ft as b,X as c,Bo as d,Uo as i,$ as l,zt as p,kt as s,At as u,$t as v}; diff --git a/app/src/main/assets/web/vue/assets/loading-C4J6hIxs.js b/app/src/main/assets/web/vue/assets/loading-C4J6hIxs.js new file mode 100755 index 000000000..574eb4f28 --- /dev/null +++ b/app/src/main/assets/web/vue/assets/loading-C4J6hIxs.js @@ -0,0 +1 @@ +import{ak as i,N as g,a9 as c,M as d,al as f,u as m}from"./vendor-KSDcS24u.js";const L=(s,t,l=i)=>{const a=d(!1);let r=null;const o=()=>a.value=!1,n=()=>a.value=!0;g(a,e=>{if(!e)return r==null?void 0:r.close();r=f.service({target:m(s),spinner:l,text:t,lock:!0,background:"rgba(0, 0, 0, 0)"})});const u=e=>{if(!(e instanceof Promise))throw TypeError("loadingWrapper argument must be Promise");return n(),e.finally(o)};return c(()=>{o()}),{isLoading:a,showLoading:n,closeLoading:o,loadingWrapper:u}};export{L as u}; diff --git a/app/src/main/assets/web/vue/assets/loading-DkQYEuap.css b/app/src/main/assets/web/vue/assets/loading-DkQYEuap.css new file mode 100755 index 000000000..a950fa923 --- /dev/null +++ b/app/src/main/assets/web/vue/assets/loading-DkQYEuap.css @@ -0,0 +1 @@ +.el-loading-spinner{font-size:36px;color:#b5b5b5}.el-loading-text{font-weight:500;color:#b5b5b5!important} diff --git a/app/src/main/assets/web/vue/assets/popfont-WaOB0hHG.ttf b/app/src/main/assets/web/vue/assets/popfont-WaOB0hHG.ttf new file mode 100755 index 0000000000000000000000000000000000000000..e620a106dc9e48cd109614354020df8be3c12380 GIT binary patch literal 4184 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 M3#zI7*@R#J7r=v^^Z)<= literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/vue/assets/shelffont-D-W4UqG-.ttf b/app/src/main/assets/web/vue/assets/shelffont-D-W4UqG-.ttf new file mode 100755 index 0000000000000000000000000000000000000000..476a2418d31de89f1306edbb9f2cbbf7f0a42f4d GIT binary patch literal 10072 zcmd6Nd5|2}dEYy_U*FR`M|aP?bM4ID*X-;7y9;25-Nk(@xgdbWK>!4H7XWby1SwJx zEk-UUC6E$nMxqYOc15I1R6WM5N^Z+4@oqzmRiTJoe_%c4&`i+gNYkyt(G|rD;%s<>X_vj7MM+`!gzr}TL zuH}!Bn zf%Ek%*Uz83xPI;$&Oe3oeOJ#taf8jXM{)jFI4@m0cXjRUt>z8fsz8Rujq8uzy8Fij z_cli4^&2NUZ<{`z{Ja#VbIzc*Q z?5<7Tz56c7l6T22%=Y6)DLUc!l>d1m^-9Qp$C+&9G5Rm?JxxkzrN|hWC8rKKht?0h z@9=}iet7cO$1$tj?KkiI{wwL1fB)qVe6IT0_kHHAPp3cipI-2vF5PI#RmB`}bWa-RUeJFW>2`meNO$XIAf&&fmUW+Ko%Q&!T-XUAm+8pS`2h zt9x#r+cWw)VI%urC&u#e*C_qi>g)9Gue?qc3txht@iPzMG9M|GmM$&cp=WWTjG$wv zjFvi5+J!lHJ#f6bTDo1jz4yZH(yr3Qa~JL?{n0lrtleH6FWn(4$1maYNF-Bkv$?jq zI*WOPoCmjH`0Z89aJe@FzN4=3|A9f;$bPJ?zI^=t<9D83%-!iMuI9?+($bwbmXF_g zV-b2-#VCDKQb_#NrF2hb12XGFxNdf5S-~8bVij7!oH%3k@|`zs-_G5}N}`@>`E^Qq z9ayg%!~2(Br_igs)Tx$pvZGqAmLbRLBIdS6_OBdYf+XeD(XOp<-~c0OLg8D|OFek@ zcHoyrNLX*ow1ZGIQk?ld;|@#ujQL62$cTk;pCn(wI9HBrbw>uRX|3*y`*+c$B}$jd zHt?*ntb0>mX@Yib*v(OPX$psA)`E3*7HRxr%`jRLb{ zBZ-Z?DdnWZ#XT?Lo&q85X3!3`>P&m4Rc}-@5eB}{y0WG;QC|uS+kA@7)@UJRaz&wu zI8|qN@U0D>)s^kD17W#F<3gyyqcl0Hu&L2ZJbFe9YkC3C7{D`HGmW2}9Ljj_L1ECG zX;05gQ<{q#3Z4^BP<87(YtK@?Fwf*;6@{CL3O#0PgH;+6s;bII4sbR;tZU;<#_9=5 zU7-loqyrMI8Bwv$PmnJIm7(Xb6*OAaR=epp{i-k8exs$`m!>Nxv$-70&Mp`x&3Lif zw_{$0nw|CaNKfyQe?a~q(o+LBR<)Vt&x(zul}zLESv8rTSNOsl?@Fr}@hYX2^lpBu zvOT5D&8e&(5_+mC6oXAnM6rPpsVBJ?8tjCGauzZ7W z@Ke=FtNzrkly2)!ZB3=7vkm*~bMYfWiB^5S8Z8B!3#H*XjH&8;B1X9? z+-!w~szRCT1@>S~Q1(ivL(MS7^}NIzP7JK;2Vj2N$#HUu6g#)c%BA58+cn*)x5KVRL@3&k zTB31tbZQ4}hS)dE*1!}t8!jj8G)zs4C*p3FQEl4L3sf6urie?d=k6MI30E^Dzy;i0zrAw)+y_^1FD9aF)T*;7ispH zVw!VR`MhNf*G!>k-trExLJs@;0rt8^=E*8KO|qSk+o$Xt&7ByZKiUzeS>j})zcHpj zBUmX6uR7hHj@H@=d#aEo=?8uQ0~L){*9f2}x!gdTkp=)9y=TrdEp%fb{-W!0$~IK; z@eZf7*Y@Uo*FyXrFDLZyz|maLk;U?i;&M z4~SJ@XU?sjeE8^*gGUy(&h59Q_L}upI0ZoTo70Wyd(FtThiTWmQo}R|Bd38XyRN^D zhTR*b<8P|A+Zd*;<~G{!o7)%^h|8_y2(1cu(*lM4Q#%%eW$ELdTVVSBa+BKnWY{b( zurYu83>%{rg_50F$RUK!1dvcYYxXgh>YiYnEl@@cHlXi}6*=|1fLp)Dwrfhv<~*;(z`_KT zhM}r1fRl5|2EDYBR?u+9cF>q3%#eajaK(yop43&(hm+Yl%WJw!VTri=PJ}tfw;=KgX2y&!0&?16+kru11#jhk>h z>W2rPb-DfSCLHd7Lo>LwT#s5x@GC%V|vhO6>`Dmw1C7+n4M^F~R54{Nap68jzMjME`gQFM>8 z%3;k-nAjhY*)2T4hfvFzK;Y>j+NU3qKO=QShU?_RIr7V5l)=i)3Wkp6C#fWLFPDAROV4hUKDZIq!F_V|Lr6o%Pjgaef?Wx4M3_e=VJ2NFV!Cgt@uY$O zgPI}|DP1DQU@R7y5Re6bs;(^iJcNUpEzlE-mnm=6v~1eo!t;{ABf3p6%`=$rOeL9s zEB-yom>U=gem^x*4!06WjF4Ty-+qL6c9fhT=SZnjnCTxa`}x(powSdMr2|VR+sFHk zpGb|hY84GE1{)NSQ;EwGV}3H8hwt&5d-!|LA*224`kvej*)4q#8;bcF(r4xAO#3Kp z29Dr5qKyfoG@Jp=v}lQr0IEUL94CZJ4l^U0&^T=m!3!N=OV{7CRxker%Pm-s|bZ4?8`fl|-+4t1-)caF^ zC$(~FWo>2s^q%v3F79zY`1pm>mmWT_@x)_cG4#U^OixW;Hl;=ZPSuvQY!TUVb0$(^ zPe%n9_e>LcozOs^;05)@Cc*tN{Yf^}ko-bYh%UR>+?qB59+d%4s#lZy0BAt)Lm+HD zML`N;$pPpR=w~Q~q2%6(Nqydcw*wD_;52J_nl)z?z_o2BS*4CqN~Eq)S~{bn54$96 zdtGSG`ucjoj+@%w6_zckIDD90O~5}44zxKcx=P(11G@n4~l zbRUbwJ`_*ufuVb#w2IJ~n)D=9q2Q>EFw6(X(q1B%r3ghqJ_rgy(8Ws?eD#OmtMf8; zOca$Oo7_^b)p;h=V%^vQlGP;zVwiT>D3~^asbpvo0Lc?cHr@-Sz*Sgj<Cmd84uql9z(Ou6$Z5b+$t%B2ie!IeQ&-93$bp7Bjk*0(`v!N#GO_j8)f~UL=aD_e zP+xg+@9yU8(!%_LwsGn5l`POz=06a;3v^wsE#p66GGd_sLz#5|3VW<*lSB35rF2Lj zFaEbtxFmAq7XE;qGQ_}5BYGM!Pf45_60(s^!7ihQCRS;+V-`Tj2b4itvkt&0{CVlJ zX2nS}{l2UnGQHi1Wnfz`cc!K;pws|9k)RqklwqERi_*V;0LMpkt~+cLG~Qr_K}RUf zWQ)ak#&9iEBDHK^I^YRdr5PXAD1TG9Hq@zDaSu!?^jt|g(vpC|ywaMh{}J^R7_n_S z$aReK?6hxsdQ6R93$-9PZv+n0l8ISrW-LIW%zG@tqP_T2@(_8Re1^P4UM0Un{yF(7 zBJtO;Lt8qn1FHu<@vGZj@cPHTJoe48Y&H94_Fp{x-20!q_S_$R^-sR-E^Y6;IQ_@d z-}p-HYqgI(d;RgRzwxa93L(%Jw7F^QW{)@r9nhjy4g{0J15}w z_oDfxi}xmismh5kl?;PFoq%az$e6mjOVJaiF&G#ww_1eJyh5AN!fv3%c2*h?4tL1e zq`AmY8SOA8!M>x?6Jd$OjxSSYQCJk`{bRaS6olZ8*jlSuNwqcnurG^M)) z)mVn^;5rX*hlSJ@F*Z*{JQa_}XX5=l9m{%ZT0y=`sd=C4nHZkuZlJ4_c{%_j9j3YM zD4GM=nPIPrn1vMB24_?6b4P0Cy?d8*S8(eiieU=X13JR4Mq*%bimoM)De10N#B9Ek z_INHYhSm(iO^CqLAoJa)RHt- z@EueXZ_?bDu{Fz!x^+mCUayLM37+@OGr5GnYWWFf z`*Cnk4S4^3_+*h(kZayY4uZv?`r>80zMNWCn}e!Rs$@roIDD_0g#T|tqzsReNNy?! zQB*2OgquJIuj$fPnM?Fs;u3=37NZ2ym;Bl6Xt4)7oll{R=+Hu1_B%D6DAOK8~voximQn$EA10!T(Y?qnw^&sn5GT5miucgu!!iL51@yOAOJSXgBEeeF%o;1%? z9AOL;BOJGez@!J@_sn2#-vA+2CLQVDs5AeP2yd9`8CNm#OTPgw8cSt*fx(qGnW~sh| zt9^-pzBm)n@k|VHWoy5c%!7mTJ%{~ zr1JQN3_v{Y_6Zv##&f}`yFBEZWQyDmZw42sPNbDkS=g#%CNq`!S!s{()^3*5wJJnk zSv4!jQs9KlUVz)(pnw+$vKE9SRodrf96&iugfhF85RE%iJ$-880VR)ecF}irgj$5Y z($>9h4uCa4RYeC34)UwW+*3|5V-^*g9T>YWHd)!9G84!h)EGt8Kgq|p25QVvbVn42wi{1D+Puy?V7#BpX$&TQ-ft z5NzvSS`kdb+PaBI*KAu=4VhhgW(WOAg~6dkI3>#g(s}U-@avkOCLXLaMPX&d(Z*dU zjT%RCeY6IO;W-F1+E_;BbMsi7ZL__53$~+k#IX27lp*Sraz-DLL$-fv&c?f%?O=W} zn?jjdj6s>N;MGJzVX`K|<6Qzgr|3mlTFiBeK=o7#8uolReA;KCZkn(;MGU;bivj?M zqAjXyOBtqd#Ro#xDfevq3uY$S)viu_#E<`q_z{5lcG;u!`Hx7ik8686<6FkpFJ_M& z7N&k_>%c~FLq-!s`G^~ChJ&ag*@$>Y*p$kTGV{oddl`icI4xN$k@k3xOrjHE2Br+7 zl`4)Z63uSZiAuMKOYLb8NtE)W>hGz%hXcO&Y9B4yr;ZttIx4TwQU-->?GKGjXJY3Uh2nX5y*^;1I|>8JGe;)6DHHy3jrUM;KJ4 z8P{+)Q+Wap50a2fac|U)vQ!DF334TsjeW*)Jf48@wN?Id7mqfft|g zblE-Ruy^nNZZrYjN8);Krrvcz+^FN1@s9E(u=$rztA7b^fnUNqx|cBiz4pC62h71E z&k!5;>BN!m>{01Az%YvVN4}4{1fH+sjg}U*eVk+2(UAhqA0WR(-XMQL6Lb%Kn0}VN zMZd=g+sA%~clcY%P31qUkE?$uUeR{yG5v}$WnQ*~^@9C?{coJxvE#9C#NNS7Qs^4_ zQ%Ee2?rNb;hTrT&J@S_@U)x-33w_Zt@#*2sQ@70^-%_v5NeOaw`xT72(rcqWPd?ad z3+UzPUR%TH=df~k9Ok}E{&lZSNr^7?+IY80*LrPEI`q+ATfvxr(rbgy((m-z&>+1x zk47%iA1qwI@#M`*8y9a4oF6RhT)TAn(zT7!uIrCoyRde%v~*?d{H>eUuU$I-Xldd4 z*vNrf7sjS0CZ~3tSUtCL{o2Op;?Bv@-8=U`u(t8om2)?Hx6S_aW#iuUpSo>wY+_>9 zi4zN}EBi;~G?SwfJD>c&5~B3>v!DE{-+dLIZ@>KccV7I`JHP$XcR&75-~R3A-}&rE z{`%9;zx^w(zWvZ>r7e+n?YIXtr zH*vm%%~`|PTR6Lp{!3^-iheoc7=EU509P)cHHA{}BtCb6R;;49)KJYy2y zyYamruMO59!DE>99A^B2ts{-6J}$>=9f6VbdUAoD8Z+7DUoNh9T&fLu@h zKh#8NB|P-%O1Ros`_d!lA8Val-Y{NVzW>S!e$pm?*@ffo*CZDG%u&NHb%wh?&!Hdv EUs=^u6aWAK literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/vue/assets/vendor-CXe1BRiH.css b/app/src/main/assets/web/vue/assets/vendor-CXe1BRiH.css new file mode 100755 index 000000000..0b73411d1 --- /dev/null +++ b/app/src/main/assets/web/vue/assets/vendor-CXe1BRiH.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--el-color-white:#ffffff;--el-color-black:#000000;--el-color-primary-rgb:64,158,255;--el-color-success-rgb:103,194,58;--el-color-warning-rgb:230,162,60;--el-color-danger-rgb:245,108,108;--el-color-error-rgb:245,108,108;--el-color-info-rgb:144,147,153;--el-font-size-extra-large:20px;--el-font-size-large:18px;--el-font-size-medium:16px;--el-font-size-base:14px;--el-font-size-small:13px;--el-font-size-extra-small:12px;--el-font-family:"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;--el-font-weight-primary:500;--el-font-line-height-primary:24px;--el-index-normal:1;--el-index-top:1000;--el-index-popper:2000;--el-border-radius-base:4px;--el-border-radius-small:2px;--el-border-radius-round:20px;--el-border-radius-circle:100%;--el-transition-duration:.3s;--el-transition-duration-fast:.2s;--el-transition-function-ease-in-out-bezier:cubic-bezier(.645,.045,.355,1);--el-transition-function-fast-bezier:cubic-bezier(.23,1,.32,1);--el-transition-all:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);--el-transition-fade:opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-md-fade:transform var(--el-transition-duration) var(--el-transition-function-fast-bezier),opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier);--el-transition-fade-linear:opacity var(--el-transition-duration-fast) linear;--el-transition-border:border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-box-shadow:box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-transition-color:color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier);--el-component-size-large:40px;--el-component-size:32px;--el-component-size-small:24px;color-scheme:light;--el-color-primary:#409eff;--el-color-primary-light-3:rgb(121.3,187.1,255);--el-color-primary-light-5:rgb(159.5,206.5,255);--el-color-primary-light-7:rgb(197.7,225.9,255);--el-color-primary-light-8:rgb(216.8,235.6,255);--el-color-primary-light-9:rgb(235.9,245.3,255);--el-color-primary-dark-2:rgb(51.2,126.4,204);--el-color-success:#67c23a;--el-color-success-light-3:rgb(148.6,212.3,117.1);--el-color-success-light-5:rgb(179,224.5,156.5);--el-color-success-light-7:rgb(209.4,236.7,195.9);--el-color-success-light-8:rgb(224.6,242.8,215.6);--el-color-success-light-9:rgb(239.8,248.9,235.3);--el-color-success-dark-2:rgb(82.4,155.2,46.4);--el-color-warning:#e6a23c;--el-color-warning-light-3:rgb(237.5,189.9,118.5);--el-color-warning-light-5:rgb(242.5,208.5,157.5);--el-color-warning-light-7:rgb(247.5,227.1,196.5);--el-color-warning-light-8:rgb(250,236.4,216);--el-color-warning-light-9:rgb(252.5,245.7,235.5);--el-color-warning-dark-2:rgb(184,129.6,48);--el-color-danger:#f56c6c;--el-color-danger-light-3:rgb(248,152.1,152.1);--el-color-danger-light-5:rgb(250,181.5,181.5);--el-color-danger-light-7:rgb(252,210.9,210.9);--el-color-danger-light-8:rgb(253,225.6,225.6);--el-color-danger-light-9:rgb(254,240.3,240.3);--el-color-danger-dark-2:rgb(196,86.4,86.4);--el-color-error:#f56c6c;--el-color-error-light-3:rgb(248,152.1,152.1);--el-color-error-light-5:rgb(250,181.5,181.5);--el-color-error-light-7:rgb(252,210.9,210.9);--el-color-error-light-8:rgb(253,225.6,225.6);--el-color-error-light-9:rgb(254,240.3,240.3);--el-color-error-dark-2:rgb(196,86.4,86.4);--el-color-info:#909399;--el-color-info-light-3:rgb(177.3,179.4,183.6);--el-color-info-light-5:rgb(199.5,201,204);--el-color-info-light-7:rgb(221.7,222.6,224.4);--el-color-info-light-8:rgb(232.8,233.4,234.6);--el-color-info-light-9:rgb(243.9,244.2,244.8);--el-color-info-dark-2:rgb(115.2,117.6,122.4);--el-bg-color:#ffffff;--el-bg-color-page:#f2f3f5;--el-bg-color-overlay:#ffffff;--el-text-color-primary:#303133;--el-text-color-regular:#606266;--el-text-color-secondary:#909399;--el-text-color-placeholder:#a8abb2;--el-text-color-disabled:#c0c4cc;--el-border-color:#dcdfe6;--el-border-color-light:#e4e7ed;--el-border-color-lighter:#ebeef5;--el-border-color-extra-light:#f2f6fc;--el-border-color-dark:#d4d7de;--el-border-color-darker:#cdd0d6;--el-fill-color:#f0f2f5;--el-fill-color-light:#f5f7fa;--el-fill-color-lighter:#fafafa;--el-fill-color-extra-light:#fafcff;--el-fill-color-dark:#ebedf0;--el-fill-color-darker:#e6e8eb;--el-fill-color-blank:#ffffff;--el-box-shadow:0px 12px 32px 4px rgba(0,0,0,.04),0px 8px 20px rgba(0,0,0,.08);--el-box-shadow-light:0px 0px 12px rgba(0,0,0,.12);--el-box-shadow-lighter:0px 0px 6px rgba(0,0,0,.12);--el-box-shadow-dark:0px 16px 48px 16px rgba(0,0,0,.08),0px 12px 32px rgba(0,0,0,.12),0px 8px 16px -8px rgba(0,0,0,.16);--el-disabled-bg-color:var(--el-fill-color-light);--el-disabled-text-color:var(--el-text-color-placeholder);--el-disabled-border-color:var(--el-border-color-light);--el-overlay-color:rgba(0,0,0,.8);--el-overlay-color-light:rgba(0,0,0,.7);--el-overlay-color-lighter:rgba(0,0,0,.5);--el-mask-color:rgba(255,255,255,.9);--el-mask-color-extra-light:rgba(255,255,255,.3);--el-border-width:1px;--el-border-style:solid;--el-border-color-hover:var(--el-text-color-disabled);--el-border:var(--el-border-width) var(--el-border-style) var(--el-border-color);--el-svg-monochrome-grey:var(--el-border-color)}.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.fade-in-linear-enter-from,.fade-in-linear-leave-to{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.el-fade-in-linear-enter-from,.el-fade-in-linear-leave-to{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-fade-in-enter-from,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter-from,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transform:scaleY(1);transform-origin:center top;transition:var(--el-transition-md-fade)}.el-zoom-in-top-enter-active[data-popper-placement^=top],.el-zoom-in-top-leave-active[data-popper-placement^=top]{transform-origin:center bottom}.el-zoom-in-top-enter-from,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transform:scaleY(1);transform-origin:center bottom;transition:var(--el-transition-md-fade)}.el-zoom-in-bottom-enter-from,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transform:scale(1);transform-origin:top left;transition:var(--el-transition-md-fade)}.el-zoom-in-left-enter-from,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45)}.collapse-transition{transition:var(--el-transition-duration) height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.el-collapse-transition-enter-active,.el-collapse-transition-leave-active{transition:var(--el-transition-duration) max-height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.horizontal-collapse-transition{transition:var(--el-transition-duration) width ease-in-out,var(--el-transition-duration) padding-left ease-in-out,var(--el-transition-duration) padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter-from,.el-list-leave-to{opacity:0;transform:translateY(-30px)}.el-list-leave-active{position:absolute!important}.el-opacity-transition{transition:opacity var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-icon-loading{animation:rotating 2s linear infinite}.el-icon--right{margin-left:5px}.el-icon--left{margin-right:5px}@keyframes rotating{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.el-icon{--color:inherit;align-items:center;display:inline-flex;height:1em;justify-content:center;line-height:1em;position:relative;width:1em;fill:currentColor;color:var(--color);font-size:inherit}.el-icon.is-loading{animation:rotating 2s linear infinite}.el-icon svg{height:1em;width:1em}.el-tabs{--el-tabs-header-height:40px;display:flex}.el-tabs__header{align-items:center;display:flex;justify-content:space-between;margin:0 0 15px;padding:0;position:relative}.el-tabs__header-vertical{flex-direction:column}.el-tabs__active-bar{background-color:var(--el-color-primary);bottom:0;height:2px;left:0;list-style:none;position:absolute;transition:width var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier),transform var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier);z-index:1}.el-tabs__new-tab{align-items:center;border:1px solid var(--el-border-color);border-radius:3px;color:var(--el-text-color-primary);cursor:pointer;display:flex;font-size:12px;height:20px;justify-content:center;line-height:20px;margin:10px 0 10px 10px;text-align:center;transition:all .15s;width:20px}.el-tabs__new-tab .is-icon-plus{height:inherit;transform:scale(.8);width:inherit}.el-tabs__new-tab .is-icon-plus svg{vertical-align:middle}.el-tabs__new-tab:hover{color:var(--el-color-primary)}.el-tabs__new-tab-vertical{margin-left:0}.el-tabs__nav-wrap{flex:1 auto;margin-bottom:-1px;overflow:hidden;position:relative}.el-tabs__nav-wrap:after{background-color:var(--el-border-color-light);bottom:0;content:"";height:2px;left:0;position:absolute;width:100%;z-index:var(--el-index-normal)}.el-tabs__nav-wrap.is-scrollable{box-sizing:border-box;padding:0 20px}.el-tabs__nav-scroll{overflow:hidden}.el-tabs__nav-next,.el-tabs__nav-prev{color:var(--el-text-color-secondary);cursor:pointer;font-size:12px;line-height:44px;position:absolute;text-align:center;width:20px}.el-tabs__nav-next{right:0}.el-tabs__nav-prev{left:0}.el-tabs__nav{display:flex;float:left;position:relative;transition:transform var(--el-transition-duration);white-space:nowrap;z-index:calc(var(--el-index-normal) + 1)}.el-tabs__nav.is-stretch{display:flex;min-width:100%}.el-tabs__nav.is-stretch>*{flex:1;text-align:center}.el-tabs__item{align-items:center;box-sizing:border-box;color:var(--el-text-color-primary);display:flex;font-size:var(--el-font-size-base);font-weight:500;height:var(--el-tabs-header-height);justify-content:center;list-style:none;padding:0 20px;position:relative}.el-tabs__item:focus,.el-tabs__item:focus:active{outline:none}.el-tabs__item:focus-visible{border-radius:3px;box-shadow:0 0 2px 2px var(--el-color-primary) inset}.el-tabs__item .is-icon-close{border-radius:50%;margin-left:5px;text-align:center;transition:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier)}.el-tabs__item .is-icon-close:before{display:inline-block;transform:scale(.9)}.el-tabs__item .is-icon-close:hover{background-color:var(--el-text-color-placeholder);color:#fff}.el-tabs__item.is-active,.el-tabs__item:hover{color:var(--el-color-primary)}.el-tabs__item:hover{cursor:pointer}.el-tabs__item.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-tabs__content{flex-grow:1;overflow:hidden;position:relative}.el-tabs--bottom>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:0}.el-tabs--bottom>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top>.el-tabs__header .el-tabs__item:last-child{padding-right:0}.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:nth-child(2),.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:nth-child(2){padding-left:20px}.el-tabs--bottom.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--bottom.el-tabs--card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--border-card>.el-tabs__header .el-tabs__item:last-child,.el-tabs--top.el-tabs--card>.el-tabs__header .el-tabs__item:last-child{padding-right:20px}.el-tabs--card>.el-tabs__header{border-bottom:1px solid var(--el-border-color-light);height:var(--el-tabs-header-height)}.el-tabs--card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs--card>.el-tabs__header .el-tabs__nav{border:1px solid var(--el-border-color-light);border-bottom:none;border-radius:4px 4px 0 0;box-sizing:border-box}.el-tabs--card>.el-tabs__header .el-tabs__active-bar{display:none}.el-tabs--card>.el-tabs__header .el-tabs__item .is-icon-close{font-size:12px;height:14px;overflow:hidden;position:relative;right:-2px;transform-origin:100% 50%;width:0}.el-tabs--card>.el-tabs__header .el-tabs__item{border-bottom:1px solid transparent;border-left:1px solid var(--el-border-color-light);transition:color var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier),padding var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier)}.el-tabs--card>.el-tabs__header .el-tabs__item:first-child{border-left:none}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover{padding-left:13px;padding-right:13px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-closable:hover .is-icon-close{width:14px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{border-bottom-color:var(--el-bg-color)}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable{padding-left:20px;padding-right:20px}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active.is-closable .is-icon-close{width:14px}.el-tabs--border-card{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color)}.el-tabs--border-card>.el-tabs__content{padding:15px}.el-tabs--border-card>.el-tabs__header{background-color:var(--el-fill-color-light);border-bottom:1px solid var(--el-border-color-light);margin:0}.el-tabs--border-card>.el-tabs__header .el-tabs__nav-wrap:after{content:none}.el-tabs--border-card>.el-tabs__header .el-tabs__item{border:1px solid transparent;color:var(--el-text-color-secondary);margin-top:-1px;transition:all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier)}.el-tabs--border-card>.el-tabs__header .el-tabs__item+.el-tabs__item,.el-tabs--border-card>.el-tabs__header .el-tabs__item:first-child{margin-left:-1px}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active{background-color:var(--el-bg-color-overlay);border-left-color:var(--el-border-color);border-right-color:var(--el-border-color);color:var(--el-color-primary)}.el-tabs--border-card>.el-tabs__header .el-tabs__item:not(.is-disabled):hover{color:var(--el-color-primary)}.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-disabled{color:var(--el-disabled-text-color)}.el-tabs--border-card>.el-tabs__header .is-scrollable .el-tabs__item:first-child{margin-left:0}.el-tabs--bottom{flex-direction:column}.el-tabs--bottom .el-tabs__header.is-bottom{margin-bottom:0;margin-top:10px}.el-tabs--bottom.el-tabs--border-card .el-tabs__header.is-bottom{border-bottom:0;border-top:1px solid var(--el-border-color)}.el-tabs--bottom.el-tabs--border-card .el-tabs__nav-wrap.is-bottom{margin-bottom:0;margin-top:-1px}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom:not(.is-active){border:1px solid transparent}.el-tabs--bottom.el-tabs--border-card .el-tabs__item.is-bottom{margin:0 -1px -1px}.el-tabs--left,.el-tabs--right{overflow:hidden}.el-tabs--left .el-tabs__header.is-left,.el-tabs--left .el-tabs__header.is-right,.el-tabs--left .el-tabs__nav-scroll,.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__header.is-left,.el-tabs--right .el-tabs__header.is-right,.el-tabs--right .el-tabs__nav-scroll,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{height:100%}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__active-bar.is-right,.el-tabs--right .el-tabs__active-bar.is-left,.el-tabs--right .el-tabs__active-bar.is-right{bottom:auto;height:auto;top:0;width:2px}.el-tabs--left .el-tabs__nav-wrap.is-left,.el-tabs--left .el-tabs__nav-wrap.is-right,.el-tabs--right .el-tabs__nav-wrap.is-left,.el-tabs--right .el-tabs__nav-wrap.is-right{margin-bottom:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{cursor:pointer;height:30px;line-height:30px;text-align:center;width:100%}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next i,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev i{transform:rotate(90deg)}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-prev,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-prev{left:auto;top:0}.el-tabs--left .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--left .el-tabs__nav-wrap.is-right>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-left>.el-tabs__nav-next,.el-tabs--right .el-tabs__nav-wrap.is-right>.el-tabs__nav-next{bottom:0;right:auto}.el-tabs--left .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--left .el-tabs__nav-wrap.is-right.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-left.is-scrollable,.el-tabs--right .el-tabs__nav-wrap.is-right.is-scrollable{padding:30px 0}.el-tabs--left .el-tabs__nav-wrap.is-left:after,.el-tabs--left .el-tabs__nav-wrap.is-right:after,.el-tabs--right .el-tabs__nav-wrap.is-left:after,.el-tabs--right .el-tabs__nav-wrap.is-right:after{bottom:auto;height:100%;top:0;width:2px}.el-tabs--left .el-tabs__nav.is-left,.el-tabs--left .el-tabs__nav.is-right,.el-tabs--right .el-tabs__nav.is-left,.el-tabs--right .el-tabs__nav.is-right{flex-direction:column}.el-tabs--left .el-tabs__item.is-left,.el-tabs--right .el-tabs__item.is-left{justify-content:flex-end}.el-tabs--left .el-tabs__item.is-right,.el-tabs--right .el-tabs__item.is-right{justify-content:flex-start}.el-tabs--left{flex-direction:row-reverse}.el-tabs--left .el-tabs__header.is-left{margin-bottom:0;margin-right:10px}.el-tabs--left .el-tabs__nav-wrap.is-left{margin-right:-1px}.el-tabs--left .el-tabs__active-bar.is-left,.el-tabs--left .el-tabs__nav-wrap.is-left:after{left:auto;right:0}.el-tabs--left .el-tabs__item.is-left{text-align:right}.el-tabs--left.el-tabs--card .el-tabs__active-bar.is-left{display:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left{border-bottom:none;border-left:none;border-right:1px solid var(--el-border-color-light);border-top:1px solid var(--el-border-color-light);text-align:left}.el-tabs--left.el-tabs--card .el-tabs__item.is-left:first-child{border-right:1px solid var(--el-border-color-light);border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active{border:1px solid var(--el-border-color-light);border-bottom:none;border-left:none;border-right:1px solid #fff}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:first-child{border-top:none}.el-tabs--left.el-tabs--card .el-tabs__item.is-left.is-active:last-child{border-bottom:none}.el-tabs--left.el-tabs--card .el-tabs__nav{border-bottom:1px solid var(--el-border-color-light);border-radius:4px 0 0 4px;border-right:none}.el-tabs--left.el-tabs--card .el-tabs__new-tab{float:none}.el-tabs--left.el-tabs--border-card .el-tabs__header.is-left{border-right:1px solid var(--el-border-color)}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left{border:1px solid transparent;margin:-1px 0 -1px -1px}.el-tabs--left.el-tabs--border-card .el-tabs__item.is-left.is-active{border-color:rgb(209,219,229) transparent}.el-tabs--right .el-tabs__header.is-right{margin-bottom:0;margin-left:10px}.el-tabs--right .el-tabs__nav-wrap.is-right{margin-left:-1px}.el-tabs--right .el-tabs__nav-wrap.is-right:after{left:0;right:auto}.el-tabs--right .el-tabs__active-bar.is-right{left:0}.el-tabs--right.el-tabs--card .el-tabs__active-bar.is-right{display:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right{border-bottom:none;border-top:1px solid var(--el-border-color-light)}.el-tabs--right.el-tabs--card .el-tabs__item.is-right:first-child{border-left:1px solid var(--el-border-color-light);border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active{border:1px solid var(--el-border-color-light);border-bottom:none;border-left:1px solid #fff;border-right:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:first-child{border-top:none}.el-tabs--right.el-tabs--card .el-tabs__item.is-right.is-active:last-child{border-bottom:none}.el-tabs--right.el-tabs--card .el-tabs__nav{border-bottom:1px solid var(--el-border-color-light);border-left:none;border-radius:0 4px 4px 0}.el-tabs--right.el-tabs--border-card .el-tabs__header.is-right{border-left:1px solid var(--el-border-color)}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right{border:1px solid transparent;margin:-1px -1px -1px 0}.el-tabs--right.el-tabs--border-card .el-tabs__item.is-right.is-active{border-color:rgb(209,219,229) transparent}.el-tabs--top{flex-direction:column-reverse}.slideInLeft-transition,.slideInRight-transition{display:inline-block}.slideInRight-enter{animation:slideInRight-enter var(--el-transition-duration)}.slideInRight-leave{animation:slideInRight-leave var(--el-transition-duration);left:0;position:absolute;right:0}.slideInLeft-enter{animation:slideInLeft-enter var(--el-transition-duration)}.slideInLeft-leave{animation:slideInLeft-leave var(--el-transition-duration);left:0;position:absolute;right:0}@keyframes slideInRight-enter{0%{opacity:0;transform:translate(100%);transform-origin:0 0}to{opacity:1;transform:translate(0);transform-origin:0 0}}@keyframes slideInRight-leave{0%{opacity:1;transform:translate(0);transform-origin:0 0}to{opacity:0;transform:translate(100%);transform-origin:0 0}}@keyframes slideInLeft-enter{0%{opacity:0;transform:translate(-100%);transform-origin:0 0}to{opacity:1;transform:translate(0);transform-origin:0 0}}@keyframes slideInLeft-leave{0%{opacity:1;transform:translate(0);transform-origin:0 0}to{opacity:0;transform:translate(-100%);transform-origin:0 0}}.el-text{--el-text-font-size:var(--el-font-size-base);--el-text-color:var(--el-text-color-regular);align-self:center;color:var(--el-text-color);font-size:var(--el-text-font-size);margin:0;overflow-wrap:break-word;padding:0}.el-text.is-truncated{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.el-text.is-line-clamp{display:-webkit-inline-box;-webkit-box-orient:vertical;overflow:hidden}.el-text--large{--el-text-font-size:var(--el-font-size-medium)}.el-text--default{--el-text-font-size:var(--el-font-size-base)}.el-text--small{--el-text-font-size:var(--el-font-size-extra-small)}.el-text.el-text--primary{--el-text-color:var(--el-color-primary)}.el-text.el-text--success{--el-text-color:var(--el-color-success)}.el-text.el-text--warning{--el-text-color:var(--el-color-warning)}.el-text.el-text--danger{--el-text-color:var(--el-color-danger)}.el-text.el-text--error{--el-text-color:var(--el-color-error)}.el-text.el-text--info{--el-text-color:var(--el-color-info)}.el-text>.el-icon{vertical-align:-2px}.el-link{--el-link-font-size:var(--el-font-size-base);--el-link-font-weight:var(--el-font-weight-primary);--el-link-text-color:var(--el-text-color-regular);--el-link-hover-text-color:var(--el-color-primary);--el-link-disabled-text-color:var(--el-text-color-placeholder);align-items:center;color:var(--el-link-text-color);cursor:pointer;display:inline-flex;flex-direction:row;font-size:var(--el-link-font-size);font-weight:var(--el-link-font-weight);justify-content:center;outline:none;padding:0;position:relative;text-decoration:none;vertical-align:middle}.el-link:hover{color:var(--el-link-hover-text-color)}.el-link.is-underline:hover:after{border-bottom:1px solid var(--el-link-hover-text-color);bottom:0;content:"";height:0;left:0;position:absolute;right:0}.el-link.is-disabled{color:var(--el-link-disabled-text-color);cursor:not-allowed}.el-link [class*=el-icon-]+span{margin-left:5px}.el-link.el-link--default:after{border-color:var(--el-link-hover-text-color)}.el-link__inner{align-items:center;display:inline-flex;justify-content:center}.el-link.el-link--primary{--el-link-text-color:var(--el-color-primary);--el-link-hover-text-color:var(--el-color-primary-light-3);--el-link-disabled-text-color:var(--el-color-primary-light-5)}.el-link.el-link--primary.is-underline:hover:after,.el-link.el-link--primary:after{border-color:var(--el-link-text-color)}.el-link.el-link--success{--el-link-text-color:var(--el-color-success);--el-link-hover-text-color:var(--el-color-success-light-3);--el-link-disabled-text-color:var(--el-color-success-light-5)}.el-link.el-link--success.is-underline:hover:after,.el-link.el-link--success:after{border-color:var(--el-link-text-color)}.el-link.el-link--warning{--el-link-text-color:var(--el-color-warning);--el-link-hover-text-color:var(--el-color-warning-light-3);--el-link-disabled-text-color:var(--el-color-warning-light-5)}.el-link.el-link--warning.is-underline:hover:after,.el-link.el-link--warning:after{border-color:var(--el-link-text-color)}.el-link.el-link--danger{--el-link-text-color:var(--el-color-danger);--el-link-hover-text-color:var(--el-color-danger-light-3);--el-link-disabled-text-color:var(--el-color-danger-light-5)}.el-link.el-link--danger.is-underline:hover:after,.el-link.el-link--danger:after{border-color:var(--el-link-text-color)}.el-link.el-link--error{--el-link-text-color:var(--el-color-error);--el-link-hover-text-color:var(--el-color-error-light-3);--el-link-disabled-text-color:var(--el-color-error-light-5)}.el-link.el-link--error.is-underline:hover:after,.el-link.el-link--error:after{border-color:var(--el-link-text-color)}.el-link.el-link--info{--el-link-text-color:var(--el-color-info);--el-link-hover-text-color:var(--el-color-info-light-3);--el-link-disabled-text-color:var(--el-color-info-light-5)}.el-link.el-link--info.is-underline:hover:after,.el-link.el-link--info:after{border-color:var(--el-link-text-color)}.el-checkbox-group{font-size:0;line-height:0}.el-button{--el-button-font-weight:var(--el-font-weight-primary);--el-button-border-color:var(--el-border-color);--el-button-bg-color:var(--el-fill-color-blank);--el-button-text-color:var(--el-text-color-regular);--el-button-disabled-text-color:var(--el-disabled-text-color);--el-button-disabled-bg-color:var(--el-fill-color-blank);--el-button-disabled-border-color:var(--el-border-color-light);--el-button-divide-border-color:rgba(255,255,255,.5);--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-color-primary-light-9);--el-button-hover-border-color:var(--el-color-primary-light-7);--el-button-active-text-color:var(--el-button-hover-text-color);--el-button-active-border-color:var(--el-color-primary);--el-button-active-bg-color:var(--el-button-hover-bg-color);--el-button-outline-color:var(--el-color-primary-light-5);--el-button-hover-link-text-color:var(--el-color-info);--el-button-active-color:var(--el-text-color-primary);align-items:center;-webkit-appearance:none;background-color:var(--el-button-bg-color);border:var(--el-border);border-color:var(--el-button-border-color);box-sizing:border-box;color:var(--el-button-text-color);cursor:pointer;display:inline-flex;font-weight:var(--el-button-font-weight);height:32px;justify-content:center;line-height:1;outline:none;text-align:center;transition:.1s;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;white-space:nowrap}.el-button:hover{background-color:var(--el-button-hover-bg-color);border-color:var(--el-button-hover-border-color);color:var(--el-button-hover-text-color);outline:none}.el-button:active{background-color:var(--el-button-active-bg-color);border-color:var(--el-button-active-border-color);color:var(--el-button-active-text-color);outline:none}.el-button:focus-visible{outline:2px solid var(--el-button-outline-color);outline-offset:1px;transition:outline-offset 0s,outline 0s}.el-button>span{align-items:center;display:inline-flex}.el-button+.el-button{margin-left:12px}.el-button{border-radius:var(--el-border-radius-base);font-size:var(--el-font-size-base)}.el-button,.el-button.is-round{padding:8px 15px}.el-button::-moz-focus-inner{border:0}.el-button [class*=el-icon]+span{margin-left:6px}.el-button [class*=el-icon] svg{vertical-align:bottom}.el-button.is-plain{--el-button-hover-text-color:var(--el-color-primary);--el-button-hover-bg-color:var(--el-fill-color-blank);--el-button-hover-border-color:var(--el-color-primary)}.el-button.is-active{background-color:var(--el-button-active-bg-color);border-color:var(--el-button-active-border-color);color:var(--el-button-active-text-color);outline:none}.el-button.is-disabled,.el-button.is-disabled:hover{background-color:var(--el-button-disabled-bg-color);background-image:none;border-color:var(--el-button-disabled-border-color);color:var(--el-button-disabled-text-color);cursor:not-allowed}.el-button.is-loading{pointer-events:none;position:relative}.el-button.is-loading:before{background-color:var(--el-mask-color-extra-light);border-radius:inherit;bottom:-1px;content:"";left:-1px;pointer-events:none;position:absolute;right:-1px;top:-1px;z-index:1}.el-button.is-round{border-radius:var(--el-border-radius-round)}.el-button.is-circle{border-radius:50%;padding:8px;width:32px}.el-button.is-text{background-color:transparent;border:0 solid transparent;color:var(--el-button-text-color)}.el-button.is-text.is-disabled{background-color:transparent!important;color:var(--el-button-disabled-text-color)}.el-button.is-text:not(.is-disabled):hover{background-color:var(--el-fill-color-light)}.el-button.is-text:not(.is-disabled):focus-visible{outline:2px solid var(--el-button-outline-color);outline-offset:1px;transition:outline-offset 0s,outline 0s}.el-button.is-text:not(.is-disabled):active{background-color:var(--el-fill-color)}.el-button.is-text:not(.is-disabled).is-has-bg{background-color:var(--el-fill-color-light)}.el-button.is-text:not(.is-disabled).is-has-bg:hover{background-color:var(--el-fill-color)}.el-button.is-text:not(.is-disabled).is-has-bg:active{background-color:var(--el-fill-color-dark)}.el-button__text--expand{letter-spacing:.3em;margin-right:-.3em}.el-button.is-link{background:transparent;border-color:transparent;color:var(--el-button-text-color);height:auto;padding:2px}.el-button.is-link:hover{color:var(--el-button-hover-link-text-color)}.el-button.is-link.is-disabled{background-color:transparent!important;border-color:transparent!important;color:var(--el-button-disabled-text-color)}.el-button.is-link:not(.is-disabled):active,.el-button.is-link:not(.is-disabled):hover{background-color:transparent;border-color:transparent}.el-button.is-link:not(.is-disabled):active{color:var(--el-button-active-color)}.el-button--text{background:transparent;border-color:transparent;color:var(--el-color-primary);padding-left:0;padding-right:0}.el-button--text.is-disabled{background-color:transparent!important;border-color:transparent!important;color:var(--el-button-disabled-text-color)}.el-button--text:not(.is-disabled):hover{background-color:transparent;border-color:transparent;color:var(--el-color-primary-light-3)}.el-button--text:not(.is-disabled):active{background-color:transparent;border-color:transparent;color:var(--el-color-primary-dark-2)}.el-button__link--expand{letter-spacing:.3em;margin-right:-.3em}.el-button--primary{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-primary);--el-button-border-color:var(--el-color-primary);--el-button-outline-color:var(--el-color-primary-light-5);--el-button-active-color:var(--el-color-primary-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-primary-light-5);--el-button-hover-bg-color:var(--el-color-primary-light-3);--el-button-hover-border-color:var(--el-color-primary-light-3);--el-button-active-bg-color:var(--el-color-primary-dark-2);--el-button-active-border-color:var(--el-color-primary-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-primary-light-5);--el-button-disabled-border-color:var(--el-color-primary-light-5)}.el-button--primary.is-link,.el-button--primary.is-plain,.el-button--primary.is-text{--el-button-text-color:var(--el-color-primary);--el-button-bg-color:var(--el-color-primary-light-9);--el-button-border-color:var(--el-color-primary-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-primary);--el-button-hover-border-color:var(--el-color-primary);--el-button-active-text-color:var(--el-color-white)}.el-button--primary.is-link.is-disabled,.el-button--primary.is-link.is-disabled:active,.el-button--primary.is-link.is-disabled:focus,.el-button--primary.is-link.is-disabled:hover,.el-button--primary.is-plain.is-disabled,.el-button--primary.is-plain.is-disabled:active,.el-button--primary.is-plain.is-disabled:focus,.el-button--primary.is-plain.is-disabled:hover,.el-button--primary.is-text.is-disabled,.el-button--primary.is-text.is-disabled:active,.el-button--primary.is-text.is-disabled:focus,.el-button--primary.is-text.is-disabled:hover{background-color:var(--el-color-primary-light-9);border-color:var(--el-color-primary-light-8);color:var(--el-color-primary-light-5)}.el-button--success{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-success);--el-button-border-color:var(--el-color-success);--el-button-outline-color:var(--el-color-success-light-5);--el-button-active-color:var(--el-color-success-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-success-light-5);--el-button-hover-bg-color:var(--el-color-success-light-3);--el-button-hover-border-color:var(--el-color-success-light-3);--el-button-active-bg-color:var(--el-color-success-dark-2);--el-button-active-border-color:var(--el-color-success-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-success-light-5);--el-button-disabled-border-color:var(--el-color-success-light-5)}.el-button--success.is-link,.el-button--success.is-plain,.el-button--success.is-text{--el-button-text-color:var(--el-color-success);--el-button-bg-color:var(--el-color-success-light-9);--el-button-border-color:var(--el-color-success-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-success);--el-button-hover-border-color:var(--el-color-success);--el-button-active-text-color:var(--el-color-white)}.el-button--success.is-link.is-disabled,.el-button--success.is-link.is-disabled:active,.el-button--success.is-link.is-disabled:focus,.el-button--success.is-link.is-disabled:hover,.el-button--success.is-plain.is-disabled,.el-button--success.is-plain.is-disabled:active,.el-button--success.is-plain.is-disabled:focus,.el-button--success.is-plain.is-disabled:hover,.el-button--success.is-text.is-disabled,.el-button--success.is-text.is-disabled:active,.el-button--success.is-text.is-disabled:focus,.el-button--success.is-text.is-disabled:hover{background-color:var(--el-color-success-light-9);border-color:var(--el-color-success-light-8);color:var(--el-color-success-light-5)}.el-button--warning{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-warning);--el-button-border-color:var(--el-color-warning);--el-button-outline-color:var(--el-color-warning-light-5);--el-button-active-color:var(--el-color-warning-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-warning-light-5);--el-button-hover-bg-color:var(--el-color-warning-light-3);--el-button-hover-border-color:var(--el-color-warning-light-3);--el-button-active-bg-color:var(--el-color-warning-dark-2);--el-button-active-border-color:var(--el-color-warning-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-warning-light-5);--el-button-disabled-border-color:var(--el-color-warning-light-5)}.el-button--warning.is-link,.el-button--warning.is-plain,.el-button--warning.is-text{--el-button-text-color:var(--el-color-warning);--el-button-bg-color:var(--el-color-warning-light-9);--el-button-border-color:var(--el-color-warning-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-warning);--el-button-hover-border-color:var(--el-color-warning);--el-button-active-text-color:var(--el-color-white)}.el-button--warning.is-link.is-disabled,.el-button--warning.is-link.is-disabled:active,.el-button--warning.is-link.is-disabled:focus,.el-button--warning.is-link.is-disabled:hover,.el-button--warning.is-plain.is-disabled,.el-button--warning.is-plain.is-disabled:active,.el-button--warning.is-plain.is-disabled:focus,.el-button--warning.is-plain.is-disabled:hover,.el-button--warning.is-text.is-disabled,.el-button--warning.is-text.is-disabled:active,.el-button--warning.is-text.is-disabled:focus,.el-button--warning.is-text.is-disabled:hover{background-color:var(--el-color-warning-light-9);border-color:var(--el-color-warning-light-8);color:var(--el-color-warning-light-5)}.el-button--danger{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-danger);--el-button-border-color:var(--el-color-danger);--el-button-outline-color:var(--el-color-danger-light-5);--el-button-active-color:var(--el-color-danger-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-danger-light-5);--el-button-hover-bg-color:var(--el-color-danger-light-3);--el-button-hover-border-color:var(--el-color-danger-light-3);--el-button-active-bg-color:var(--el-color-danger-dark-2);--el-button-active-border-color:var(--el-color-danger-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-danger-light-5);--el-button-disabled-border-color:var(--el-color-danger-light-5)}.el-button--danger.is-link,.el-button--danger.is-plain,.el-button--danger.is-text{--el-button-text-color:var(--el-color-danger);--el-button-bg-color:var(--el-color-danger-light-9);--el-button-border-color:var(--el-color-danger-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-danger);--el-button-hover-border-color:var(--el-color-danger);--el-button-active-text-color:var(--el-color-white)}.el-button--danger.is-link.is-disabled,.el-button--danger.is-link.is-disabled:active,.el-button--danger.is-link.is-disabled:focus,.el-button--danger.is-link.is-disabled:hover,.el-button--danger.is-plain.is-disabled,.el-button--danger.is-plain.is-disabled:active,.el-button--danger.is-plain.is-disabled:focus,.el-button--danger.is-plain.is-disabled:hover,.el-button--danger.is-text.is-disabled,.el-button--danger.is-text.is-disabled:active,.el-button--danger.is-text.is-disabled:focus,.el-button--danger.is-text.is-disabled:hover{background-color:var(--el-color-danger-light-9);border-color:var(--el-color-danger-light-8);color:var(--el-color-danger-light-5)}.el-button--info{--el-button-text-color:var(--el-color-white);--el-button-bg-color:var(--el-color-info);--el-button-border-color:var(--el-color-info);--el-button-outline-color:var(--el-color-info-light-5);--el-button-active-color:var(--el-color-info-dark-2);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-link-text-color:var(--el-color-info-light-5);--el-button-hover-bg-color:var(--el-color-info-light-3);--el-button-hover-border-color:var(--el-color-info-light-3);--el-button-active-bg-color:var(--el-color-info-dark-2);--el-button-active-border-color:var(--el-color-info-dark-2);--el-button-disabled-text-color:var(--el-color-white);--el-button-disabled-bg-color:var(--el-color-info-light-5);--el-button-disabled-border-color:var(--el-color-info-light-5)}.el-button--info.is-link,.el-button--info.is-plain,.el-button--info.is-text{--el-button-text-color:var(--el-color-info);--el-button-bg-color:var(--el-color-info-light-9);--el-button-border-color:var(--el-color-info-light-5);--el-button-hover-text-color:var(--el-color-white);--el-button-hover-bg-color:var(--el-color-info);--el-button-hover-border-color:var(--el-color-info);--el-button-active-text-color:var(--el-color-white)}.el-button--info.is-link.is-disabled,.el-button--info.is-link.is-disabled:active,.el-button--info.is-link.is-disabled:focus,.el-button--info.is-link.is-disabled:hover,.el-button--info.is-plain.is-disabled,.el-button--info.is-plain.is-disabled:active,.el-button--info.is-plain.is-disabled:focus,.el-button--info.is-plain.is-disabled:hover,.el-button--info.is-text.is-disabled,.el-button--info.is-text.is-disabled:active,.el-button--info.is-text.is-disabled:focus,.el-button--info.is-text.is-disabled:hover{background-color:var(--el-color-info-light-9);border-color:var(--el-color-info-light-8);color:var(--el-color-info-light-5)}.el-button--large{--el-button-size:40px;height:var(--el-button-size)}.el-button--large [class*=el-icon]+span{margin-left:8px}.el-button--large{border-radius:var(--el-border-radius-base);font-size:var(--el-font-size-base);padding:12px 19px}.el-button--large.is-round{padding:12px 19px}.el-button--large.is-circle{padding:12px;width:var(--el-button-size)}.el-button--small{--el-button-size:24px;height:var(--el-button-size)}.el-button--small [class*=el-icon]+span{margin-left:4px}.el-button--small{border-radius:calc(var(--el-border-radius-base) - 1px);font-size:12px;padding:5px 11px}.el-button--small.is-round{padding:5px 11px}.el-button--small.is-circle{padding:5px;width:var(--el-button-size)}.el-textarea{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;display:inline-block;font-size:var(--el-font-size-base);position:relative;vertical-align:bottom;width:100%}.el-textarea__inner{-webkit-appearance:none;background-color:var(--el-input-bg-color,var(--el-fill-color-blank));background-image:none;border:none;border-radius:var(--el-input-border-radius,var(--el-border-radius-base));box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset;box-sizing:border-box;color:var(--el-input-text-color,var(--el-text-color-regular));display:block;font-family:inherit;font-size:inherit;line-height:1.5;padding:5px 11px;position:relative;resize:vertical;transition:var(--el-transition-box-shadow);width:100%}.el-textarea__inner::-moz-placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-textarea__inner::placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-textarea__inner:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-textarea__inner:focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset;outline:none}.el-textarea .el-input__count{background:var(--el-fill-color-blank);bottom:5px;color:var(--el-color-info);font-size:12px;line-height:14px;position:absolute;right:10px}.el-textarea.is-disabled .el-textarea__inner{background-color:var(--el-disabled-bg-color);box-shadow:0 0 0 1px var(--el-disabled-border-color) inset;color:var(--el-disabled-text-color);cursor:not-allowed}.el-textarea.is-disabled .el-textarea__inner::-moz-placeholder{color:var(--el-text-color-placeholder)}.el-textarea.is-disabled .el-textarea__inner::placeholder{color:var(--el-text-color-placeholder)}.el-textarea.is-exceed .el-textarea__inner{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-textarea.is-exceed .el-input__count{color:var(--el-color-danger)}.el-input{--el-input-text-color:var(--el-text-color-regular);--el-input-border:var(--el-border);--el-input-hover-border:var(--el-border-color-hover);--el-input-focus-border:var(--el-color-primary);--el-input-transparent-border:0 0 0 1px transparent inset;--el-input-border-color:var(--el-border-color);--el-input-border-radius:var(--el-border-radius-base);--el-input-bg-color:var(--el-fill-color-blank);--el-input-icon-color:var(--el-text-color-placeholder);--el-input-placeholder-color:var(--el-text-color-placeholder);--el-input-hover-border-color:var(--el-border-color-hover);--el-input-clear-hover-color:var(--el-text-color-secondary);--el-input-focus-border-color:var(--el-color-primary);--el-input-width:100%;--el-input-height:var(--el-component-size);box-sizing:border-box;display:inline-flex;font-size:var(--el-font-size-base);line-height:var(--el-input-height);position:relative;vertical-align:middle;width:var(--el-input-width)}.el-input::-webkit-scrollbar{width:6px;z-index:11}.el-input::-webkit-scrollbar:horizontal{height:6px}.el-input::-webkit-scrollbar-thumb{background:var(--el-text-color-disabled);border-radius:5px;width:6px}.el-input::-webkit-scrollbar-corner,.el-input::-webkit-scrollbar-track{background:var(--el-fill-color-blank)}.el-input::-webkit-scrollbar-track-piece{background:var(--el-fill-color-blank);width:6px}.el-input .el-input__clear,.el-input .el-input__password{color:var(--el-input-icon-color);cursor:pointer;font-size:14px}.el-input .el-input__clear:hover,.el-input .el-input__password:hover{color:var(--el-input-clear-hover-color)}.el-input .el-input__count{align-items:center;color:var(--el-color-info);display:inline-flex;font-size:12px;height:100%}.el-input .el-input__count .el-input__count-inner{background:var(--el-fill-color-blank);display:inline-block;line-height:normal;padding-left:8px}.el-input__wrapper{align-items:center;background-color:var(--el-input-bg-color,var(--el-fill-color-blank));background-image:none;border-radius:var(--el-input-border-radius,var(--el-border-radius-base));box-shadow:0 0 0 1px var(--el-input-border-color,var(--el-border-color)) inset;cursor:text;display:inline-flex;flex-grow:1;justify-content:center;padding:1px 11px;transform:translateZ(0);transition:var(--el-transition-box-shadow)}.el-input__wrapper:hover{box-shadow:0 0 0 1px var(--el-input-hover-border-color) inset}.el-input__wrapper.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-input__inner{--el-input-inner-height:calc(var(--el-input-height, 32px) - 2px);-webkit-appearance:none;background:none;border:none;box-sizing:border-box;color:var(--el-input-text-color,var(--el-text-color-regular));flex-grow:1;font-size:inherit;height:var(--el-input-inner-height);line-height:var(--el-input-inner-height);outline:none;padding:0;width:100%}.el-input__inner:focus{outline:none}.el-input__inner::-moz-placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-input__inner::placeholder{color:var(--el-input-placeholder-color,var(--el-text-color-placeholder))}.el-input__inner[type=password]::-ms-reveal{display:none}.el-input__inner[type=number]{line-height:1}.el-input__prefix{color:var(--el-input-icon-color,var(--el-text-color-placeholder));display:inline-flex;flex-shrink:0;flex-wrap:nowrap;height:100%;pointer-events:none;text-align:center;transition:all var(--el-transition-duration);white-space:nowrap}.el-input__prefix-inner{align-items:center;display:inline-flex;justify-content:center;pointer-events:all}.el-input__prefix-inner>:last-child{margin-right:8px}.el-input__prefix-inner>:first-child,.el-input__prefix-inner>:first-child.el-input__icon{margin-left:0}.el-input__suffix{color:var(--el-input-icon-color,var(--el-text-color-placeholder));display:inline-flex;flex-shrink:0;flex-wrap:nowrap;height:100%;pointer-events:none;text-align:center;transition:all var(--el-transition-duration);white-space:nowrap}.el-input__suffix-inner{align-items:center;display:inline-flex;justify-content:center;pointer-events:all}.el-input__suffix-inner>:first-child{margin-left:8px}.el-input .el-input__icon{align-items:center;display:flex;height:inherit;justify-content:center;line-height:inherit;margin-left:8px;transition:all var(--el-transition-duration)}.el-input__validateIcon{pointer-events:none}.el-input.is-active .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-color, ) inset}.el-input.is-disabled{cursor:not-allowed}.el-input.is-disabled .el-input__wrapper{background-color:var(--el-disabled-bg-color);box-shadow:0 0 0 1px var(--el-disabled-border-color) inset}.el-input.is-disabled .el-input__inner{color:var(--el-disabled-text-color);-webkit-text-fill-color:var(--el-disabled-text-color);cursor:not-allowed}.el-input.is-disabled .el-input__inner::-moz-placeholder{color:var(--el-text-color-placeholder)}.el-input.is-disabled .el-input__inner::placeholder{color:var(--el-text-color-placeholder)}.el-input.is-disabled .el-input__icon{cursor:not-allowed}.el-input.is-exceed .el-input__wrapper{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-input.is-exceed .el-input__suffix .el-input__count{color:var(--el-color-danger)}.el-input--large{--el-input-height:var(--el-component-size-large);font-size:14px}.el-input--large .el-input__wrapper{padding:1px 15px}.el-input--large .el-input__inner{--el-input-inner-height:calc(var(--el-input-height, 40px) - 2px)}.el-input--small{--el-input-height:var(--el-component-size-small);font-size:12px}.el-input--small .el-input__wrapper{padding:1px 7px}.el-input--small .el-input__inner{--el-input-inner-height:calc(var(--el-input-height, 24px) - 2px)}.el-input-group{align-items:stretch;display:inline-flex;width:100%}.el-input-group__append,.el-input-group__prepend{align-items:center;background-color:var(--el-fill-color-light);border-radius:var(--el-input-border-radius);color:var(--el-color-info);display:inline-flex;justify-content:center;min-height:100%;padding:0 20px;position:relative;white-space:nowrap}.el-input-group__append:focus,.el-input-group__prepend:focus{outline:none}.el-input-group__append .el-button,.el-input-group__append .el-select,.el-input-group__prepend .el-button,.el-input-group__prepend .el-select{display:inline-block;margin:0 -20px}.el-input-group__append button.el-button,.el-input-group__append button.el-button:hover,.el-input-group__append div.el-select .el-select__wrapper,.el-input-group__append div.el-select:hover .el-select__wrapper,.el-input-group__prepend button.el-button,.el-input-group__prepend button.el-button:hover,.el-input-group__prepend div.el-select .el-select__wrapper,.el-input-group__prepend div.el-select:hover .el-select__wrapper{background-color:transparent;border-color:transparent;color:inherit}.el-input-group__append .el-button,.el-input-group__append .el-input,.el-input-group__prepend .el-button,.el-input-group__prepend .el-input{font-size:inherit}.el-input-group__prepend{border-bottom-right-radius:0;border-right:0;border-top-right-radius:0;box-shadow:1px 0 0 0 var(--el-input-border-color) inset,0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset}.el-input-group__append{border-left:0;box-shadow:0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset,-1px 0 0 0 var(--el-input-border-color) inset}.el-input-group--prepend>.el-input__wrapper,.el-input-group__append{border-bottom-left-radius:0;border-top-left-radius:0}.el-input-group--prepend .el-input-group__prepend .el-select .el-select__wrapper{border-bottom-right-radius:0;border-top-right-radius:0;box-shadow:1px 0 0 0 var(--el-input-border-color) inset,0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset}.el-input-group--append>.el-input__wrapper{border-bottom-right-radius:0;border-top-right-radius:0}.el-input-group--append .el-input-group__append .el-select .el-select__wrapper{border-bottom-left-radius:0;border-top-left-radius:0;box-shadow:0 1px 0 0 var(--el-input-border-color) inset,0 -1px 0 0 var(--el-input-border-color) inset,-1px 0 0 0 var(--el-input-border-color) inset}.el-input-hidden{display:none!important}.el-badge{--el-badge-bg-color:var(--el-color-danger);--el-badge-radius:10px;--el-badge-font-size:12px;--el-badge-padding:6px;--el-badge-size:18px;display:inline-block;position:relative;vertical-align:middle;width:-moz-fit-content;width:fit-content}.el-badge__content{align-items:center;background-color:var(--el-badge-bg-color);border:1px solid var(--el-bg-color);border-radius:var(--el-badge-radius);color:var(--el-color-white);display:inline-flex;font-size:var(--el-badge-font-size);height:var(--el-badge-size);justify-content:center;padding:0 var(--el-badge-padding);white-space:nowrap}.el-badge__content.is-fixed{position:absolute;right:calc(1px + var(--el-badge-size)/2);top:0;transform:translateY(-50%) translate(100%);z-index:var(--el-index-normal)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{border-radius:50%;height:8px;padding:0;right:0;width:8px}.el-badge__content.is-hide-zero{display:none}.el-badge__content--primary{background-color:var(--el-color-primary)}.el-badge__content--success{background-color:var(--el-color-success)}.el-badge__content--warning{background-color:var(--el-color-warning)}.el-badge__content--info{background-color:var(--el-color-info)}.el-badge__content--danger{background-color:var(--el-color-danger)}.el-message{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-border-color-lighter);--el-message-padding:11px 15px;--el-message-close-size:16px;--el-message-close-icon-color:var(--el-text-color-placeholder);--el-message-close-hover-color:var(--el-text-color-secondary);align-items:center;background-color:var(--el-message-bg-color);border-color:var(--el-message-border-color);border-radius:var(--el-border-radius-base);border-style:var(--el-border-style);border-width:var(--el-border-width);box-sizing:border-box;display:flex;gap:8px;left:50%;max-width:calc(100% - 32px);padding:var(--el-message-padding);position:fixed;top:20px;transform:translate(-50%);transition:opacity var(--el-transition-duration),transform .4s,top .4s;width:-moz-fit-content;width:fit-content}.el-message.is-center{justify-content:center}.el-message.is-plain{background-color:var(--el-bg-color-overlay);border-color:var(--el-bg-color-overlay);box-shadow:var(--el-box-shadow-light)}.el-message p{margin:0}.el-message--success{--el-message-bg-color:var(--el-color-success-light-9);--el-message-border-color:var(--el-color-success-light-8);--el-message-text-color:var(--el-color-success)}.el-message--success .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--success{color:var(--el-message-text-color)}.el-message--info{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-color-info-light-8);--el-message-text-color:var(--el-color-info)}.el-message--info .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--info{color:var(--el-message-text-color)}.el-message--warning{--el-message-bg-color:var(--el-color-warning-light-9);--el-message-border-color:var(--el-color-warning-light-8);--el-message-text-color:var(--el-color-warning)}.el-message--warning .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--warning{color:var(--el-message-text-color)}.el-message--error{--el-message-bg-color:var(--el-color-error-light-9);--el-message-border-color:var(--el-color-error-light-8);--el-message-text-color:var(--el-color-error)}.el-message--error .el-message__content{color:var(--el-message-text-color);overflow-wrap:break-word}.el-message .el-message-icon--error{color:var(--el-message-text-color)}.el-message .el-message__badge{position:absolute;right:-8px;top:-8px}.el-message__content{font-size:14px;line-height:1;padding:0}.el-message__content:focus{outline-width:0}.el-message .el-message__closeBtn{color:var(--el-message-close-icon-color);cursor:pointer;font-size:var(--el-message-close-size)}.el-message .el-message__closeBtn:focus{outline-width:0}.el-message .el-message__closeBtn:hover{color:var(--el-message-close-hover-color)}.el-message-fade-enter-from,.el-message-fade-leave-to{opacity:0;transform:translate(-50%,-100%)}.el-checkbox{--el-checkbox-font-size:14px;--el-checkbox-font-weight:var(--el-font-weight-primary);--el-checkbox-text-color:var(--el-text-color-regular);--el-checkbox-input-height:14px;--el-checkbox-input-width:14px;--el-checkbox-border-radius:var(--el-border-radius-small);--el-checkbox-bg-color:var(--el-fill-color-blank);--el-checkbox-input-border:var(--el-border);--el-checkbox-disabled-border-color:var(--el-border-color);--el-checkbox-disabled-input-fill:var(--el-fill-color-light);--el-checkbox-disabled-icon-color:var(--el-text-color-placeholder);--el-checkbox-disabled-checked-input-fill:var(--el-border-color-extra-light);--el-checkbox-disabled-checked-input-border-color:var(--el-border-color);--el-checkbox-disabled-checked-icon-color:var(--el-text-color-placeholder);--el-checkbox-checked-text-color:var(--el-color-primary);--el-checkbox-checked-input-border-color:var(--el-color-primary);--el-checkbox-checked-bg-color:var(--el-color-primary);--el-checkbox-checked-icon-color:var(--el-color-white);--el-checkbox-input-border-color-hover:var(--el-color-primary);align-items:center;color:var(--el-checkbox-text-color);cursor:pointer;display:inline-flex;font-size:var(--el-font-size-base);font-weight:var(--el-checkbox-font-weight);height:var(--el-checkbox-height,32px);margin-right:30px;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.el-checkbox.is-disabled{cursor:not-allowed}.el-checkbox.is-bordered{border:var(--el-border);border-radius:var(--el-border-radius-base);box-sizing:border-box;padding:0 15px 0 9px}.el-checkbox.is-bordered.is-checked{border-color:var(--el-color-primary)}.el-checkbox.is-bordered.is-disabled{border-color:var(--el-border-color-lighter)}.el-checkbox.is-bordered.el-checkbox--large{border-radius:var(--el-border-radius-base);padding:0 19px 0 11px}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__label{font-size:var(--el-font-size-base)}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{border-radius:calc(var(--el-border-radius-base) - 1px);padding:0 11px 0 7px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox input:focus-visible+.el-checkbox__inner{border-radius:var(--el-checkbox-border-radius);outline:2px solid var(--el-checkbox-input-border-color-hover);outline-offset:1px}.el-checkbox__input{cursor:pointer;display:inline-flex;outline:none;position:relative;white-space:nowrap}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:var(--el-checkbox-disabled-input-fill);border-color:var(--el-checkbox-disabled-border-color);cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{border-color:var(--el-checkbox-disabled-icon-color);cursor:not-allowed}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:var(--el-checkbox-disabled-checked-icon-color);border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:var(--el-disabled-text-color);cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-checked-icon-color);transform:rotate(45deg) scaleY(1)}.el-checkbox__input.is-checked+.el-checkbox__label{color:var(--el-checkbox-checked-text-color)}.el-checkbox__input.is-focus:not(.is-checked) .el-checkbox__original:not(:focus-visible){border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{background-color:var(--el-checkbox-checked-icon-color);content:"";display:block;height:2px;left:0;position:absolute;right:0;top:5px;transform:scale(.5)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{background-color:var(--el-checkbox-bg-color);border:var(--el-checkbox-input-border);border-radius:var(--el-checkbox-border-radius);box-sizing:border-box;display:inline-block;height:var(--el-checkbox-input-height);position:relative;transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46),outline .25s cubic-bezier(.71,-.46,.29,1.46);width:var(--el-checkbox-input-width);z-index:var(--el-index-normal)}.el-checkbox__inner:hover{border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__inner:after{border:1px solid transparent;border-left:0;border-top:0;box-sizing:content-box;content:"";height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);transform-origin:center;transition:transform .15s ease-in .05s;width:3px}.el-checkbox__original{height:0;margin:0;opacity:0;outline:none;position:absolute;width:0;z-index:-1}.el-checkbox__label{display:inline-block;font-size:var(--el-checkbox-font-size);line-height:1;padding-left:8px}.el-checkbox.el-checkbox--large{height:40px}.el-checkbox.el-checkbox--large .el-checkbox__label{font-size:14px}.el-checkbox.el-checkbox--large .el-checkbox__inner{height:14px;width:14px}.el-checkbox.el-checkbox--small{height:24px}.el-checkbox.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.el-checkbox--small .el-checkbox__input.is-indeterminate .el-checkbox__inner:before{top:4px}.el-checkbox.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox:last-of-type{margin-right:0}.el-dialog{--el-dialog-width:50%;--el-dialog-margin-top:15vh;--el-dialog-bg-color:var(--el-bg-color);--el-dialog-box-shadow:var(--el-box-shadow);--el-dialog-title-font-size:var(--el-font-size-large);--el-dialog-content-font-size:14px;--el-dialog-font-line-height:var(--el-font-line-height-primary);--el-dialog-padding-primary:16px;--el-dialog-border-radius:var(--el-border-radius-base);background:var(--el-dialog-bg-color);border-radius:var(--el-dialog-border-radius);box-shadow:var(--el-dialog-box-shadow);box-sizing:border-box;margin:var(--el-dialog-margin-top,15vh) auto 50px;overflow-wrap:break-word;padding:var(--el-dialog-padding-primary);position:relative;width:var(--el-dialog-width,50%)}.el-dialog:focus{outline:none!important}.el-dialog.is-align-center{margin:auto}.el-dialog.is-fullscreen{--el-dialog-width:100%;--el-dialog-margin-top:0;height:100%;margin-bottom:0;overflow:auto}.el-dialog__wrapper{bottom:0;left:0;margin:0;overflow:auto;position:fixed;right:0;top:0}.el-dialog.is-draggable .el-dialog__header{cursor:move;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-dialog__header{padding-bottom:var(--el-dialog-padding-primary)}.el-dialog__header.show-close{padding-right:calc(var(--el-dialog-padding-primary) + var(--el-message-close-size, 16px))}.el-dialog__headerbtn{background:transparent;border:none;cursor:pointer;font-size:var(--el-message-close-size,16px);height:48px;outline:none;padding:0;position:absolute;right:0;top:0;width:48px}.el-dialog__headerbtn .el-dialog__close{color:var(--el-color-info);font-size:inherit}.el-dialog__headerbtn:focus .el-dialog__close,.el-dialog__headerbtn:hover .el-dialog__close{color:var(--el-color-primary)}.el-dialog__title{color:var(--el-text-color-primary);font-size:var(--el-dialog-title-font-size);line-height:var(--el-dialog-font-line-height)}.el-dialog__body{color:var(--el-text-color-regular);font-size:var(--el-dialog-content-font-size)}.el-dialog__footer{box-sizing:border-box;padding-top:var(--el-dialog-padding-primary);text-align:right}.el-dialog--center{text-align:center}.el-dialog--center .el-dialog__body{text-align:initial}.el-dialog--center .el-dialog__footer{text-align:inherit}.el-overlay-dialog{bottom:0;left:0;overflow:auto;position:fixed;right:0;top:0}.dialog-fade-enter-active{animation:modal-fade-in var(--el-transition-duration)}.dialog-fade-enter-active .el-overlay-dialog{animation:dialog-fade-in var(--el-transition-duration)}.dialog-fade-leave-active{animation:modal-fade-out var(--el-transition-duration)}.dialog-fade-leave-active .el-overlay-dialog{animation:dialog-fade-out var(--el-transition-duration)}@keyframes dialog-fade-in{0%{opacity:0;transform:translate3d(0,-20px,0)}to{opacity:1;transform:translateZ(0)}}@keyframes dialog-fade-out{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20px,0)}}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}@keyframes modal-fade-out{0%{opacity:1}to{opacity:0}}.el-overlay{background-color:var(--el-overlay-color-lighter);bottom:0;height:100%;left:0;overflow:auto;position:fixed;right:0;top:0;z-index:2000}.el-overlay .el-overlay-root{height:0}.el-form{--el-form-label-font-size:var(--el-font-size-base);--el-form-inline-content-width:220px}.el-form--inline .el-form-item{display:inline-flex;margin-right:32px;vertical-align:middle}.el-form--inline.el-form--label-top{display:flex;flex-wrap:wrap}.el-form--inline.el-form--label-top .el-form-item{display:block}.el-form-item{display:flex;--font-size:14px;margin-bottom:18px}.el-form-item .el-form-item{margin-bottom:0}.el-form-item .el-input__validateIcon{display:none}.el-form-item--large{--font-size:14px;--el-form-label-font-size:var(--font-size);margin-bottom:22px}.el-form-item--large .el-form-item__label{height:40px;line-height:40px}.el-form-item--large .el-form-item__content{line-height:40px}.el-form-item--large .el-form-item__error{padding-top:4px}.el-form-item--default{--font-size:14px;--el-form-label-font-size:var(--font-size);margin-bottom:18px}.el-form-item--default .el-form-item__label{height:32px;line-height:32px}.el-form-item--default .el-form-item__content{line-height:32px}.el-form-item--default .el-form-item__error{padding-top:2px}.el-form-item--small{--font-size:12px;--el-form-label-font-size:var(--font-size);margin-bottom:18px}.el-form-item--small .el-form-item__label{height:24px;line-height:24px}.el-form-item--small .el-form-item__content{line-height:24px}.el-form-item--small .el-form-item__error{padding-top:2px}.el-form-item--label-left .el-form-item__label{justify-content:flex-start}.el-form-item--label-top{display:block}.el-form-item--label-top .el-form-item__label{display:inline-block;height:auto;line-height:22px;margin-bottom:8px;text-align:left;vertical-align:middle}.el-form-item__label-wrap{display:flex}.el-form-item__label{align-items:flex-start;box-sizing:border-box;color:var(--el-text-color-regular);display:inline-flex;flex:0 0 auto;font-size:var(--el-form-label-font-size);height:32px;justify-content:flex-end;line-height:32px;padding:0 12px 0 0}.el-form-item__content{align-items:center;display:flex;flex:1;flex-wrap:wrap;font-size:var(--font-size);line-height:32px;min-width:0;position:relative}.el-form-item__content .el-input-group{vertical-align:top}.el-form-item__error{color:var(--el-color-danger);font-size:12px;left:0;line-height:1;padding-top:2px;position:absolute;top:100%}.el-form-item__error--inline{display:inline-block;left:auto;margin-left:10px;position:relative;top:auto}.el-form-item.is-required:not(.is-no-asterisk).asterisk-left>.el-form-item__label-wrap>.el-form-item__label:before,.el-form-item.is-required:not(.is-no-asterisk).asterisk-left>.el-form-item__label:before{color:var(--el-color-danger);content:"*";margin-right:4px}.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label-wrap>.el-form-item__label:after,.el-form-item.is-required:not(.is-no-asterisk).asterisk-right>.el-form-item__label:after{color:var(--el-color-danger);content:"*";margin-left:4px}.el-form-item.is-error .el-input__wrapper,.el-form-item.is-error .el-input__wrapper.is-focus,.el-form-item.is-error .el-input__wrapper:focus,.el-form-item.is-error .el-input__wrapper:hover,.el-form-item.is-error .el-select__wrapper,.el-form-item.is-error .el-select__wrapper.is-focus,.el-form-item.is-error .el-select__wrapper:focus,.el-form-item.is-error .el-select__wrapper:hover,.el-form-item.is-error .el-textarea__inner,.el-form-item.is-error .el-textarea__inner.is-focus,.el-form-item.is-error .el-textarea__inner:focus,.el-form-item.is-error .el-textarea__inner:hover{box-shadow:0 0 0 1px var(--el-color-danger) inset}.el-form-item.is-error .el-input-group__append .el-input__wrapper,.el-form-item.is-error .el-input-group__prepend .el-input__wrapper{box-shadow:inset 0 0 0 1px transparent}.el-form-item.is-error .el-input-group__append .el-input__validateIcon,.el-form-item.is-error .el-input-group__prepend .el-input__validateIcon{display:none}.el-form-item.is-error .el-input__validateIcon{color:var(--el-color-danger)}.el-form-item--feedback .el-input__validateIcon{display:inline-flex}.el-tag{--el-tag-font-size:12px;--el-tag-border-radius:4px;--el-tag-border-radius-rounded:9999px;align-items:center;background-color:var(--el-tag-bg-color);border-color:var(--el-tag-border-color);border-radius:var(--el-tag-border-radius);border-style:solid;border-width:1px;box-sizing:border-box;color:var(--el-tag-text-color);display:inline-flex;font-size:var(--el-tag-font-size);height:24px;justify-content:center;line-height:1;padding:0 9px;vertical-align:middle;white-space:nowrap;--el-icon-size:14px}.el-tag,.el-tag.el-tag--primary{--el-tag-bg-color:var(--el-color-primary-light-9);--el-tag-border-color:var(--el-color-primary-light-8);--el-tag-hover-color:var(--el-color-primary)}.el-tag.el-tag--success{--el-tag-bg-color:var(--el-color-success-light-9);--el-tag-border-color:var(--el-color-success-light-8);--el-tag-hover-color:var(--el-color-success)}.el-tag.el-tag--warning{--el-tag-bg-color:var(--el-color-warning-light-9);--el-tag-border-color:var(--el-color-warning-light-8);--el-tag-hover-color:var(--el-color-warning)}.el-tag.el-tag--danger{--el-tag-bg-color:var(--el-color-danger-light-9);--el-tag-border-color:var(--el-color-danger-light-8);--el-tag-hover-color:var(--el-color-danger)}.el-tag.el-tag--error{--el-tag-bg-color:var(--el-color-error-light-9);--el-tag-border-color:var(--el-color-error-light-8);--el-tag-hover-color:var(--el-color-error)}.el-tag.el-tag--info{--el-tag-bg-color:var(--el-color-info-light-9);--el-tag-border-color:var(--el-color-info-light-8);--el-tag-hover-color:var(--el-color-info)}.el-tag.is-hit{border-color:var(--el-color-primary)}.el-tag.is-round{border-radius:var(--el-tag-border-radius-rounded)}.el-tag .el-tag__close{color:var(--el-tag-text-color);flex-shrink:0}.el-tag .el-tag__close:hover{background-color:var(--el-tag-hover-color);color:var(--el-color-white)}.el-tag.el-tag--primary{--el-tag-text-color:var(--el-color-primary)}.el-tag.el-tag--success{--el-tag-text-color:var(--el-color-success)}.el-tag.el-tag--warning{--el-tag-text-color:var(--el-color-warning)}.el-tag.el-tag--danger{--el-tag-text-color:var(--el-color-danger)}.el-tag.el-tag--error{--el-tag-text-color:var(--el-color-error)}.el-tag.el-tag--info{--el-tag-text-color:var(--el-color-info)}.el-tag .el-icon{border-radius:50%;cursor:pointer;font-size:calc(var(--el-icon-size) - 2px);height:var(--el-icon-size);width:var(--el-icon-size)}.el-tag .el-tag__close{margin-left:6px}.el-tag--dark{--el-tag-text-color:var(--el-color-white)}.el-tag--dark,.el-tag--dark.el-tag--primary{--el-tag-bg-color:var(--el-color-primary);--el-tag-border-color:var(--el-color-primary);--el-tag-hover-color:var(--el-color-primary-light-3)}.el-tag--dark.el-tag--success{--el-tag-bg-color:var(--el-color-success);--el-tag-border-color:var(--el-color-success);--el-tag-hover-color:var(--el-color-success-light-3)}.el-tag--dark.el-tag--warning{--el-tag-bg-color:var(--el-color-warning);--el-tag-border-color:var(--el-color-warning);--el-tag-hover-color:var(--el-color-warning-light-3)}.el-tag--dark.el-tag--danger{--el-tag-bg-color:var(--el-color-danger);--el-tag-border-color:var(--el-color-danger);--el-tag-hover-color:var(--el-color-danger-light-3)}.el-tag--dark.el-tag--error{--el-tag-bg-color:var(--el-color-error);--el-tag-border-color:var(--el-color-error);--el-tag-hover-color:var(--el-color-error-light-3)}.el-tag--dark.el-tag--info{--el-tag-bg-color:var(--el-color-info);--el-tag-border-color:var(--el-color-info);--el-tag-hover-color:var(--el-color-info-light-3)}.el-tag--dark.el-tag--danger,.el-tag--dark.el-tag--error,.el-tag--dark.el-tag--info,.el-tag--dark.el-tag--primary,.el-tag--dark.el-tag--success,.el-tag--dark.el-tag--warning{--el-tag-text-color:var(--el-color-white)}.el-tag--plain,.el-tag--plain.el-tag--primary{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-primary-light-5);--el-tag-hover-color:var(--el-color-primary)}.el-tag--plain.el-tag--success{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-success-light-5);--el-tag-hover-color:var(--el-color-success)}.el-tag--plain.el-tag--warning{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-warning-light-5);--el-tag-hover-color:var(--el-color-warning)}.el-tag--plain.el-tag--danger{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-danger-light-5);--el-tag-hover-color:var(--el-color-danger)}.el-tag--plain.el-tag--error{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-error-light-5);--el-tag-hover-color:var(--el-color-error)}.el-tag--plain.el-tag--info{--el-tag-bg-color:var(--el-fill-color-blank);--el-tag-border-color:var(--el-color-info-light-5);--el-tag-hover-color:var(--el-color-info)}.el-tag.is-closable{padding-right:5px}.el-tag--large{height:32px;padding:0 11px;--el-icon-size:16px}.el-tag--large .el-tag__close{margin-left:8px}.el-tag--large.is-closable{padding-right:7px}.el-tag--small{height:20px;padding:0 7px;--el-icon-size:12px}.el-tag--small .el-tag__close{margin-left:4px}.el-tag--small.is-closable{padding-right:3px}.el-tag--small .el-icon-close{transform:scale(.8)}.el-tag.el-tag--primary.is-hit{border-color:var(--el-color-primary)}.el-tag.el-tag--success.is-hit{border-color:var(--el-color-success)}.el-tag.el-tag--warning.is-hit{border-color:var(--el-color-warning)}.el-tag.el-tag--danger.is-hit{border-color:var(--el-color-danger)}.el-tag.el-tag--error.is-hit{border-color:var(--el-color-error)}.el-tag.el-tag--info.is-hit{border-color:var(--el-color-info)}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-selected:after{background-color:var(--el-color-primary);background-position:50%;background-repeat:no-repeat;border-right:none;border-top:none;content:"";height:12px;mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") no-repeat;mask-size:100% 100%;-webkit-mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") no-repeat;-webkit-mask-size:100% 100%;position:absolute;right:20px;top:50%;transform:translateY(-50%);width:12px}.el-scrollbar{--el-scrollbar-opacity:.3;--el-scrollbar-bg-color:var(--el-text-color-secondary);--el-scrollbar-hover-opacity:.5;--el-scrollbar-hover-bg-color:var(--el-text-color-secondary);height:100%;overflow:hidden;position:relative}.el-scrollbar__wrap{height:100%;overflow:auto}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{display:none}.el-scrollbar__thumb{background-color:var(--el-scrollbar-bg-color,var(--el-text-color-secondary));border-radius:inherit;cursor:pointer;display:block;height:0;opacity:var(--el-scrollbar-opacity,.3);position:relative;transition:var(--el-transition-duration) background-color;width:0}.el-scrollbar__thumb:hover{background-color:var(--el-scrollbar-hover-bg-color,var(--el-text-color-secondary));opacity:var(--el-scrollbar-hover-opacity,.5)}.el-scrollbar__bar{border-radius:4px;bottom:2px;position:absolute;right:2px;z-index:1}.el-scrollbar__bar.is-vertical{top:2px;width:6px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-scrollbar-fade-enter-active{transition:opacity .34s ease-out}.el-scrollbar-fade-leave-active{transition:opacity .12s ease-out}.el-scrollbar-fade-enter-from,.el-scrollbar-fade-leave-active{opacity:0}.el-popper{--el-popper-border-radius:var(--el-popover-border-radius,4px);border-radius:var(--el-popper-border-radius);font-size:12px;line-height:20px;min-width:10px;overflow-wrap:break-word;padding:5px 11px;position:absolute;visibility:visible;z-index:2000}.el-popper.is-dark{color:var(--el-bg-color)}.el-popper.is-dark,.el-popper.is-dark>.el-popper__arrow:before{background:var(--el-text-color-primary);border:1px solid var(--el-text-color-primary)}.el-popper.is-dark>.el-popper__arrow:before{right:0}.el-popper.is-light,.el-popper.is-light>.el-popper__arrow:before{background:var(--el-bg-color-overlay);border:1px solid var(--el-border-color-light)}.el-popper.is-light>.el-popper__arrow:before{right:0}.el-popper.is-pure{padding:0}.el-popper__arrow,.el-popper__arrow:before{height:10px;position:absolute;width:10px;z-index:-1}.el-popper__arrow:before{background:var(--el-text-color-primary);box-sizing:border-box;content:" ";transform:rotate(45deg)}.el-popper[data-popper-placement^=top]>.el-popper__arrow{bottom:-5px}.el-popper[data-popper-placement^=top]>.el-popper__arrow:before{border-bottom-right-radius:2px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow{top:-5px}.el-popper[data-popper-placement^=bottom]>.el-popper__arrow:before{border-top-left-radius:2px}.el-popper[data-popper-placement^=left]>.el-popper__arrow{right:-5px}.el-popper[data-popper-placement^=left]>.el-popper__arrow:before{border-top-right-radius:2px}.el-popper[data-popper-placement^=right]>.el-popper__arrow{left:-5px}.el-popper[data-popper-placement^=right]>.el-popper__arrow:before{border-bottom-left-radius:2px}.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-left-color:transparent!important;border-top-color:transparent!important}.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:transparent!important;border-right-color:transparent!important}.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:transparent!important;border-left-color:transparent!important}.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-right-color:transparent!important;border-top-color:transparent!important}.el-select-dropdown{border-radius:var(--el-border-radius-base);box-sizing:border-box;z-index:calc(var(--el-index-top) + 1)}.el-select-dropdown .el-scrollbar.is-empty .el-select-dropdown__list{padding:0}.el-select-dropdown__empty,.el-select-dropdown__loading{color:var(--el-text-color-secondary);font-size:var(--el-select-font-size);margin:0;padding:10px 0;text-align:center}.el-select-dropdown__wrap{max-height:274px}.el-select-dropdown__list{box-sizing:border-box;list-style:none;margin:0;padding:6px 0}.el-select-dropdown__list.el-vl__window{margin:6px 0;padding:0}.el-select-dropdown__header{border-bottom:1px solid var(--el-border-color-light);padding:10px}.el-select-dropdown__footer{border-top:1px solid var(--el-border-color-light);padding:10px}.el-select-dropdown__item{box-sizing:border-box;color:var(--el-text-color-regular);cursor:pointer;font-size:var(--el-font-size-base);height:34px;line-height:34px;overflow:hidden;padding:0 32px 0 20px;position:relative;text-overflow:ellipsis;white-space:nowrap}.el-select-dropdown__item.is-hovering{background-color:var(--el-fill-color-light)}.el-select-dropdown__item.is-selected{color:var(--el-color-primary);font-weight:700}.el-select-dropdown__item.is-disabled{background-color:unset;color:var(--el-text-color-placeholder);cursor:not-allowed}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-selected:after{background-color:var(--el-color-primary);background-position:50%;background-repeat:no-repeat;border-right:none;border-top:none;content:"";height:12px;mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") no-repeat;mask-size:100% 100%;-webkit-mask:url("data:image/svg+xml;utf8,%3Csvg class='icon' width='200' height='200' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' d='M406.656 706.944L195.84 496.256a32 32 0 10-45.248 45.248l256 256 512-512a32 32 0 00-45.248-45.248L406.592 706.944z'%3E%3C/path%3E%3C/svg%3E") no-repeat;-webkit-mask-size:100% 100%;position:absolute;right:20px;top:50%;transform:translateY(-50%);width:12px}.el-select-dropdown.is-multiple .el-select-dropdown__item.is-disabled:after{background-color:var(--el-text-color-placeholder)}.el-select-group{margin:0;padding:0}.el-select-group__wrap{list-style:none;margin:0;padding:0;position:relative}.el-select-group__title{color:var(--el-color-info);font-size:12px;line-height:34px;padding-left:20px}.el-select-group .el-select-dropdown__item{padding-left:20px}.el-select{--el-select-border-color-hover:var(--el-border-color-hover);--el-select-disabled-color:var(--el-disabled-text-color);--el-select-disabled-border:var(--el-disabled-border-color);--el-select-font-size:var(--el-font-size-base);--el-select-close-hover-color:var(--el-text-color-secondary);--el-select-input-color:var(--el-text-color-placeholder);--el-select-multiple-input-color:var(--el-text-color-regular);--el-select-input-focus-border-color:var(--el-color-primary);--el-select-input-font-size:14px;--el-select-width:100%;display:inline-block;position:relative;vertical-align:middle;width:var(--el-select-width)}.el-select__wrapper{align-items:center;background-color:var(--el-fill-color-blank);border-radius:var(--el-border-radius-base);box-shadow:0 0 0 1px var(--el-border-color) inset;box-sizing:border-box;cursor:pointer;display:flex;font-size:14px;gap:6px;line-height:24px;min-height:32px;padding:4px 12px;position:relative;text-align:left;transform:translateZ(0);transition:var(--el-transition-duration)}.el-select__wrapper.is-filterable{cursor:text}.el-select__wrapper.is-focused{box-shadow:0 0 0 1px var(--el-color-primary) inset}.el-select__wrapper.is-hovering:not(.is-focused){box-shadow:0 0 0 1px var(--el-border-color-hover) inset}.el-select__wrapper.is-disabled{background-color:var(--el-fill-color-light);color:var(--el-text-color-placeholder);cursor:not-allowed}.el-select__wrapper.is-disabled,.el-select__wrapper.is-disabled:hover{box-shadow:0 0 0 1px var(--el-select-disabled-border) inset}.el-select__wrapper.is-disabled.is-focus{box-shadow:0 0 0 1px var(--el-input-focus-border-color) inset}.el-select__wrapper.is-disabled .el-select__selected-item{color:var(--el-select-disabled-color)}.el-select__wrapper.is-disabled .el-select__caret,.el-select__wrapper.is-disabled .el-tag{cursor:not-allowed}.el-select__prefix,.el-select__suffix{align-items:center;color:var(--el-input-icon-color,var(--el-text-color-placeholder));display:flex;flex-shrink:0;gap:6px}.el-select__caret{color:var(--el-select-input-color);cursor:pointer;font-size:var(--el-select-input-font-size);transform:rotate(0);transition:var(--el-transition-duration)}.el-select__caret.is-reverse{transform:rotate(180deg)}.el-select__selection{align-items:center;display:flex;flex:1;flex-wrap:wrap;gap:6px;min-width:0;position:relative}.el-select__selection.is-near{margin-left:-8px}.el-select__selection .el-tag{border-color:transparent;cursor:pointer}.el-select__selection .el-tag.el-tag--plain{border-color:var(--el-tag-border-color)}.el-select__selection .el-tag .el-tag__content{min-width:0}.el-select__selected-item{display:flex;flex-wrap:wrap;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-select__tags-text{line-height:normal}.el-select__placeholder,.el-select__tags-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.el-select__placeholder{color:var(--el-input-text-color,var(--el-text-color-regular));position:absolute;top:50%;transform:translateY(-50%);width:100%}.el-select__placeholder.is-transparent{color:var(--el-text-color-placeholder);-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-select__popper.el-popper{background:var(--el-bg-color-overlay);box-shadow:var(--el-box-shadow-light)}.el-select__popper.el-popper,.el-select__popper.el-popper .el-popper__arrow:before{border:1px solid var(--el-border-color-light)}.el-select__popper.el-popper[data-popper-placement^=top] .el-popper__arrow:before{border-left-color:transparent;border-top-color:transparent}.el-select__popper.el-popper[data-popper-placement^=bottom] .el-popper__arrow:before{border-bottom-color:transparent;border-right-color:transparent}.el-select__popper.el-popper[data-popper-placement^=left] .el-popper__arrow:before{border-bottom-color:transparent;border-left-color:transparent}.el-select__popper.el-popper[data-popper-placement^=right] .el-popper__arrow:before{border-right-color:transparent;border-top-color:transparent}.el-select__input-wrapper{max-width:100%}.el-select__input-wrapper.is-hidden{opacity:0;position:absolute}.el-select__input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:none;color:var(--el-select-multiple-input-color);font-family:inherit;font-size:inherit;height:24px;max-width:100%;outline:none;padding:0}.el-select__input.is-disabled{cursor:not-allowed}.el-select__input-calculator{left:0;max-width:100%;overflow:hidden;position:absolute;top:0;visibility:hidden;white-space:pre}.el-select--large .el-select__wrapper{font-size:14px;gap:6px;line-height:24px;min-height:40px;padding:8px 16px}.el-select--large .el-select__selection{gap:6px}.el-select--large .el-select__selection.is-near{margin-left:-8px}.el-select--large .el-select__prefix,.el-select--large .el-select__suffix{gap:6px}.el-select--large .el-select__input{height:24px}.el-select--small .el-select__wrapper{font-size:12px;gap:4px;line-height:20px;min-height:24px;padding:2px 8px}.el-select--small .el-select__selection{gap:4px}.el-select--small .el-select__selection.is-near{margin-left:-6px}.el-select--small .el-select__prefix,.el-select--small .el-select__suffix{gap:4px}.el-select--small .el-select__input{height:20px}.el-input-number{display:inline-flex;line-height:30px;position:relative;vertical-align:middle;width:150px}.el-input-number .el-input__wrapper{padding-left:42px;padding-right:42px}.el-input-number .el-input__inner{-webkit-appearance:none;-moz-appearance:textfield;line-height:1;text-align:center}.el-input-number .el-input__inner::-webkit-inner-spin-button,.el-input-number .el-input__inner::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.el-input-number__decrease,.el-input-number__increase{align-items:center;background:var(--el-fill-color-light);bottom:1px;color:var(--el-text-color-regular);cursor:pointer;display:flex;font-size:13px;height:auto;justify-content:center;position:absolute;top:1px;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:32px;z-index:1}.el-input-number__decrease:hover,.el-input-number__increase:hover{color:var(--el-color-primary)}.el-input-number__decrease:hover~.el-input:not(.is-disabled) .el-input__wrapper,.el-input-number__increase:hover~.el-input:not(.is-disabled) .el-input__wrapper{box-shadow:0 0 0 1px var(--el-input-focus-border-color,var(--el-color-primary)) inset}.el-input-number__decrease.is-disabled,.el-input-number__increase.is-disabled{color:var(--el-disabled-text-color);cursor:not-allowed}.el-input-number__increase{border-left:var(--el-border);border-radius:0 var(--el-border-radius-base) var(--el-border-radius-base) 0;right:1px}.el-input-number__decrease{border-radius:var(--el-border-radius-base) 0 0 var(--el-border-radius-base);border-right:var(--el-border);left:1px}.el-input-number.is-disabled .el-input-number__decrease,.el-input-number.is-disabled .el-input-number__increase{border-color:var(--el-disabled-border-color);color:var(--el-disabled-border-color)}.el-input-number.is-disabled .el-input-number__decrease:hover,.el-input-number.is-disabled .el-input-number__increase:hover{color:var(--el-disabled-border-color);cursor:not-allowed}.el-input-number--large{line-height:38px;width:180px}.el-input-number--large .el-input-number__decrease,.el-input-number--large .el-input-number__increase{font-size:14px;width:40px}.el-input-number--large .el-input--large .el-input__wrapper{padding-left:47px;padding-right:47px}.el-input-number--small{line-height:22px;width:120px}.el-input-number--small .el-input-number__decrease,.el-input-number--small .el-input-number__increase{font-size:12px;width:24px}.el-input-number--small .el-input--small .el-input__wrapper{padding-left:31px;padding-right:31px}.el-input-number--small .el-input-number__decrease [class*=el-icon],.el-input-number--small .el-input-number__increase [class*=el-icon]{transform:scale(.9)}.el-input-number.is-without-controls .el-input__wrapper{padding-left:15px;padding-right:15px}.el-input-number.is-controls-right .el-input__wrapper{padding-left:15px;padding-right:42px}.el-input-number.is-controls-right .el-input-number__decrease,.el-input-number.is-controls-right .el-input-number__increase{--el-input-number-controls-height:15px;height:var(--el-input-number-controls-height);line-height:var(--el-input-number-controls-height)}.el-input-number.is-controls-right .el-input-number__decrease [class*=el-icon],.el-input-number.is-controls-right .el-input-number__increase [class*=el-icon]{transform:scale(.8)}.el-input-number.is-controls-right .el-input-number__increase{border-bottom:var(--el-border);border-radius:0 var(--el-border-radius-base) 0 0;bottom:auto;left:auto}.el-input-number.is-controls-right .el-input-number__decrease{border-left:var(--el-border);border-radius:0 0 var(--el-border-radius-base) 0;border-right:none;left:auto;right:1px;top:auto}.el-input-number.is-controls-right[class*=large] [class*=decrease],.el-input-number.is-controls-right[class*=large] [class*=increase]{--el-input-number-controls-height:19px}.el-input-number.is-controls-right[class*=small] [class*=decrease],.el-input-number.is-controls-right[class*=small] [class*=increase]{--el-input-number-controls-height:11px}.el-switch{--el-switch-on-color:var(--el-color-primary);--el-switch-off-color:var(--el-border-color);align-items:center;display:inline-flex;font-size:14px;height:32px;line-height:20px;position:relative;vertical-align:middle}.el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label{cursor:not-allowed}.el-switch__label{color:var(--el-text-color-primary);cursor:pointer;display:inline-block;font-size:14px;font-weight:500;height:20px;transition:var(--el-transition-duration-fast);vertical-align:middle}.el-switch__label.is-active{color:var(--el-color-primary)}.el-switch__label--left{margin-right:10px}.el-switch__label--right{margin-left:10px}.el-switch__label *{display:inline-block;font-size:14px;line-height:1}.el-switch__label .el-icon{height:inherit}.el-switch__label .el-icon svg{vertical-align:middle}.el-switch__input{height:0;margin:0;opacity:0;position:absolute;width:0}.el-switch__input:focus-visible~.el-switch__core{outline:2px solid var(--el-switch-on-color);outline-offset:1px}.el-switch__core{align-items:center;background:var(--el-switch-off-color);border:1px solid var(--el-switch-border-color,var(--el-switch-off-color));border-radius:10px;box-sizing:border-box;cursor:pointer;display:inline-flex;height:20px;min-width:40px;outline:none;position:relative;transition:border-color var(--el-transition-duration),background-color var(--el-transition-duration)}.el-switch__core .el-switch__inner{align-items:center;display:flex;height:16px;justify-content:center;overflow:hidden;padding:0 4px 0 18px;transition:all var(--el-transition-duration);width:100%}.el-switch__core .el-switch__inner .is-icon,.el-switch__core .el-switch__inner .is-text{color:var(--el-color-white);font-size:12px;overflow:hidden;text-overflow:ellipsis;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.el-switch__core .el-switch__action{align-items:center;background-color:var(--el-color-white);border-radius:var(--el-border-radius-circle);color:var(--el-switch-off-color);display:flex;height:16px;justify-content:center;left:1px;position:absolute;transition:all var(--el-transition-duration);width:16px}.el-switch.is-checked .el-switch__core{background-color:var(--el-switch-on-color);border-color:var(--el-switch-border-color,var(--el-switch-on-color))}.el-switch.is-checked .el-switch__core .el-switch__action{color:var(--el-switch-on-color);left:calc(100% - 17px)}.el-switch.is-checked .el-switch__core .el-switch__inner{padding:0 18px 0 4px}.el-switch.is-disabled{opacity:.6}.el-switch--wide .el-switch__label.el-switch__label--left span{left:10px}.el-switch--wide .el-switch__label.el-switch__label--right span{right:10px}.el-switch .label-fade-enter-from,.el-switch .label-fade-leave-active{opacity:0}.el-switch--large{font-size:14px;height:40px;line-height:24px}.el-switch--large .el-switch__label{font-size:14px;height:24px}.el-switch--large .el-switch__label *{font-size:14px}.el-switch--large .el-switch__core{border-radius:12px;height:24px;min-width:50px}.el-switch--large .el-switch__core .el-switch__inner{height:20px;padding:0 6px 0 22px}.el-switch--large .el-switch__core .el-switch__action{height:20px;width:20px}.el-switch--large.is-checked .el-switch__core .el-switch__action{left:calc(100% - 21px)}.el-switch--large.is-checked .el-switch__core .el-switch__inner{padding:0 22px 0 6px}.el-switch--small{font-size:12px;height:24px;line-height:16px}.el-switch--small .el-switch__label{font-size:12px;height:16px}.el-switch--small .el-switch__label *{font-size:12px}.el-switch--small .el-switch__core{border-radius:8px;height:16px;min-width:30px}.el-switch--small .el-switch__core .el-switch__inner{height:12px;padding:0 2px 0 14px}.el-switch--small .el-switch__core .el-switch__action{height:12px;width:12px}.el-switch--small.is-checked .el-switch__core .el-switch__action{left:calc(100% - 13px)}.el-switch--small.is-checked .el-switch__core .el-switch__inner{padding:0 14px 0 2px}html.dark{color-scheme:dark;--el-color-primary:#409eff;--el-color-primary-light-3:rgb(50.8,116.6,184.5);--el-color-primary-light-5:rgb(42,89,137.5);--el-color-primary-light-7:rgb(33.2,61.4,90.5);--el-color-primary-light-8:rgb(28.8,47.6,67);--el-color-primary-light-9:rgb(24.4,33.8,43.5);--el-color-primary-dark-2:rgb(102.2,177.4,255);--el-color-success:#67c23a;--el-color-success-light-3:rgb(78.1,141.8,46.6);--el-color-success-light-5:rgb(61.5,107,39);--el-color-success-light-7:rgb(44.9,72.2,31.4);--el-color-success-light-8:rgb(36.6,54.8,27.6);--el-color-success-light-9:rgb(28.3,37.4,23.8);--el-color-success-dark-2:rgb(133.4,206.2,97.4);--el-color-warning:#e6a23c;--el-color-warning-light-3:rgb(167,119.4,48);--el-color-warning-light-5:#7d5b28;--el-color-warning-light-7:rgb(83,62.6,32);--el-color-warning-light-8:rgb(62,48.4,28);--el-color-warning-light-9:rgb(41,34.2,24);--el-color-warning-dark-2:rgb(235,180.6,99);--el-color-danger:#f56c6c;--el-color-danger-light-3:rgb(177.5,81.6,81.6);--el-color-danger-light-5:rgb(132.5,64,64);--el-color-danger-light-7:rgb(87.5,46.4,46.4);--el-color-danger-light-8:rgb(65,37.6,37.6);--el-color-danger-light-9:rgb(42.5,28.8,28.8);--el-color-danger-dark-2:rgb(247,137.4,137.4);--el-color-error:#f56c6c;--el-color-error-light-3:rgb(177.5,81.6,81.6);--el-color-error-light-5:rgb(132.5,64,64);--el-color-error-light-7:rgb(87.5,46.4,46.4);--el-color-error-light-8:rgb(65,37.6,37.6);--el-color-error-light-9:rgb(42.5,28.8,28.8);--el-color-error-dark-2:rgb(247,137.4,137.4);--el-color-info:#909399;--el-color-info-light-3:rgb(106.8,108.9,113.1);--el-color-info-light-5:rgb(82,83.5,86.5);--el-color-info-light-7:rgb(57.2,58.1,59.9);--el-color-info-light-8:rgb(44.8,45.4,46.6);--el-color-info-light-9:rgb(32.4,32.7,33.3);--el-color-info-dark-2:rgb(166.2,168.6,173.4);--el-box-shadow:0px 12px 32px 4px rgba(0,0,0,.36),0px 8px 20px rgba(0,0,0,.72);--el-box-shadow-light:0px 0px 12px rgba(0,0,0,.72);--el-box-shadow-lighter:0px 0px 6px rgba(0,0,0,.72);--el-box-shadow-dark:0px 16px 48px 16px rgba(0,0,0,.72),0px 12px 32px #000000,0px 8px 16px -8px #000000;--el-bg-color-page:#0a0a0a;--el-bg-color:#141414;--el-bg-color-overlay:#1d1e1f;--el-text-color-primary:#E5EAF3;--el-text-color-regular:#CFD3DC;--el-text-color-secondary:#A3A6AD;--el-text-color-placeholder:#8D9095;--el-text-color-disabled:#6C6E72;--el-border-color-darker:#636466;--el-border-color-dark:#58585B;--el-border-color:#4C4D4F;--el-border-color-light:#414243;--el-border-color-lighter:#363637;--el-border-color-extra-light:#2B2B2C;--el-fill-color-darker:#424243;--el-fill-color-dark:#39393A;--el-fill-color:#303030;--el-fill-color-light:#262727;--el-fill-color-lighter:#1D1D1D;--el-fill-color-extra-light:#191919;--el-fill-color-blank:transparent;--el-mask-color:rgba(0,0,0,.8);--el-mask-color-extra-light:rgba(0,0,0,.3)}html.dark .el-button{--el-button-disabled-text-color:rgba(255,255,255,.5)}html.dark .el-card{--el-card-bg-color:var(--el-bg-color-overlay)}html.dark .el-empty{--el-empty-fill-color-0:var(--el-color-black);--el-empty-fill-color-1:#4b4b52;--el-empty-fill-color-2:#36383d;--el-empty-fill-color-3:#1e1e20;--el-empty-fill-color-4:#262629;--el-empty-fill-color-5:#202124;--el-empty-fill-color-6:#212224;--el-empty-fill-color-7:#1b1c1f;--el-empty-fill-color-8:#1c1d1f;--el-empty-fill-color-9:#18181a}:root{--el-loading-spinner-size:42px;--el-loading-fullscreen-spinner-size:50px}.el-loading-parent--relative{position:relative!important}.el-loading-parent--hidden{overflow:hidden!important}.el-loading-mask{background-color:var(--el-mask-color);bottom:0;left:0;margin:0;position:absolute;right:0;top:0;transition:opacity var(--el-transition-duration);z-index:2000}.el-loading-mask.is-fullscreen{position:fixed}.el-loading-mask.is-fullscreen .el-loading-spinner{margin-top:calc((0px - var(--el-loading-fullscreen-spinner-size))/2)}.el-loading-mask.is-fullscreen .el-loading-spinner .circular{height:var(--el-loading-fullscreen-spinner-size);width:var(--el-loading-fullscreen-spinner-size)}.el-loading-spinner{margin-top:calc((0px - var(--el-loading-spinner-size))/2);position:absolute;text-align:center;top:50%;width:100%}.el-loading-spinner .el-loading-text{color:var(--el-color-primary);font-size:14px;margin:3px 0}.el-loading-spinner .circular{animation:loading-rotate 2s linear infinite;display:inline;height:var(--el-loading-spinner-size);width:var(--el-loading-spinner-size)}.el-loading-spinner .path{animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:var(--el-color-primary);stroke-linecap:round}.el-loading-spinner i{color:var(--el-color-primary)}.el-loading-fade-enter-from,.el-loading-fade-leave-to{opacity:0}@keyframes loading-rotate{to{transform:rotate(1turn)}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}}:root{--el-popup-modal-bg-color:var(--el-color-black);--el-popup-modal-opacity:.5}.v-modal-enter{animation:v-modal-in var(--el-transition-duration-fast) ease}.v-modal-leave{animation:v-modal-out var(--el-transition-duration-fast) ease forwards}@keyframes v-modal-in{0%{opacity:0}}@keyframes v-modal-out{to{opacity:0}}.v-modal{background:var(--el-popup-modal-bg-color);height:100%;left:0;opacity:var(--el-popup-modal-opacity);position:fixed;top:0;width:100%}.el-popup-parent--hidden{overflow:hidden}.el-message-box{--el-messagebox-title-color:var(--el-text-color-primary);--el-messagebox-width:420px;--el-messagebox-border-radius:4px;--el-messagebox-box-shadow:var(--el-box-shadow);--el-messagebox-font-size:var(--el-font-size-large);--el-messagebox-content-font-size:var(--el-font-size-base);--el-messagebox-content-color:var(--el-text-color-regular);--el-messagebox-error-font-size:12px;--el-messagebox-padding-primary:12px;--el-messagebox-font-line-height:var(--el-font-line-height-primary);backface-visibility:hidden;background-color:var(--el-bg-color);border-radius:var(--el-messagebox-border-radius);box-shadow:var(--el-messagebox-box-shadow);box-sizing:border-box;display:inline-block;font-size:var(--el-messagebox-font-size);max-width:var(--el-messagebox-width);overflow:hidden;overflow-wrap:break-word;padding:var(--el-messagebox-padding-primary);position:relative;text-align:left;vertical-align:middle;width:100%}.el-message-box:focus{outline:none!important}.el-overlay.is-message-box .el-overlay-message-box{bottom:0;left:0;overflow:auto;padding:16px;position:fixed;right:0;text-align:center;top:0}.el-overlay.is-message-box .el-overlay-message-box:after{content:"";display:inline-block;height:100%;vertical-align:middle;width:0}.el-message-box.is-draggable .el-message-box__header{cursor:move;-webkit-user-select:none;-moz-user-select:none;user-select:none}.el-message-box__header{padding-bottom:var(--el-messagebox-padding-primary)}.el-message-box__header.show-close{padding-right:calc(var(--el-messagebox-padding-primary) + var(--el-message-close-size, 16px))}.el-message-box__title{color:var(--el-messagebox-title-color);font-size:var(--el-messagebox-font-size);line-height:var(--el-messagebox-font-line-height)}.el-message-box__headerbtn{background:transparent;border:none;cursor:pointer;font-size:var(--el-message-close-size,16px);height:40px;outline:none;padding:0;position:absolute;right:0;top:0;width:40px}.el-message-box__headerbtn .el-message-box__close{color:var(--el-color-info);font-size:inherit}.el-message-box__headerbtn:focus .el-message-box__close,.el-message-box__headerbtn:hover .el-message-box__close{color:var(--el-color-primary)}.el-message-box__content{color:var(--el-messagebox-content-color);font-size:var(--el-messagebox-content-font-size)}.el-message-box__container{align-items:center;display:flex;gap:12px}.el-message-box__input{padding-top:12px}.el-message-box__input div.invalid>input,.el-message-box__input div.invalid>input:focus{border-color:var(--el-color-error)}.el-message-box__status{font-size:24px}.el-message-box__status.el-message-box-icon--success{--el-messagebox-color:var(--el-color-success);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--info{--el-messagebox-color:var(--el-color-info);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--warning{--el-messagebox-color:var(--el-color-warning);color:var(--el-messagebox-color)}.el-message-box__status.el-message-box-icon--error{--el-messagebox-color:var(--el-color-error);color:var(--el-messagebox-color)}.el-message-box__message{margin:0}.el-message-box__message p{line-height:var(--el-messagebox-font-line-height);margin:0}.el-message-box__errormsg{color:var(--el-color-error);font-size:var(--el-messagebox-error-font-size);line-height:var(--el-messagebox-font-line-height)}.el-message-box__btns{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-end;padding-top:var(--el-messagebox-padding-primary)}.el-message-box--center .el-message-box__title{align-items:center;display:flex;gap:6px;justify-content:center}.el-message-box--center .el-message-box__status{font-size:inherit}.el-message-box--center .el-message-box__btns,.el-message-box--center .el-message-box__container{justify-content:center}.fade-in-linear-enter-active .el-overlay-message-box{animation:msgbox-fade-in var(--el-transition-duration)}.fade-in-linear-leave-active .el-overlay-message-box{animation:msgbox-fade-in var(--el-transition-duration) reverse}@keyframes msgbox-fade-in{0%{opacity:0;transform:translate3d(0,-20px,0)}to{opacity:1;transform:translateZ(0)}}.el-popover{--el-popover-bg-color:var(--el-bg-color-overlay);--el-popover-font-size:var(--el-font-size-base);--el-popover-border-color:var(--el-border-color-lighter);--el-popover-padding:12px;--el-popover-padding-large:18px 20px;--el-popover-title-font-size:16px;--el-popover-title-text-color:var(--el-text-color-primary);--el-popover-border-radius:4px}.el-popover.el-popper{background:var(--el-popover-bg-color);border:1px solid var(--el-popover-border-color);border-radius:var(--el-popover-border-radius);box-shadow:var(--el-box-shadow-light);box-sizing:border-box;color:var(--el-text-color-regular);font-size:var(--el-popover-font-size);line-height:1.4;min-width:150px;overflow-wrap:break-word;padding:var(--el-popover-padding);z-index:var(--el-index-popper)}.el-popover.el-popper--plain{padding:var(--el-popover-padding-large)}.el-popover__title{color:var(--el-popover-title-text-color);font-size:var(--el-popover-title-font-size);line-height:1;margin-bottom:12px}.el-popover__reference:focus:hover,.el-popover__reference:focus:not(.focusing){outline-width:0}.el-popover.el-popper.is-dark{--el-popover-bg-color:var(--el-text-color-primary);--el-popover-border-color:var(--el-text-color-primary);--el-popover-title-text-color:var(--el-bg-color);color:var(--el-bg-color)}.el-popover.el-popper:focus,.el-popover.el-popper:focus:active{outline-width:0} diff --git a/app/src/main/assets/web/vue/assets/vendor-KSDcS24u.js b/app/src/main/assets/web/vue/assets/vendor-KSDcS24u.js new file mode 100755 index 000000000..d8e76f58f --- /dev/null +++ b/app/src/main/assets/web/vue/assets/vendor-KSDcS24u.js @@ -0,0 +1,54 @@ +/** +* @vue/shared v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Ku(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const Ye={},Mo=[],ht=()=>{},mh=()=>!1,Na=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),qu=e=>e.startsWith("onUpdate:"),vt=Object.assign,Wu=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},Wb=Object.prototype.hasOwnProperty,Ve=(e,t)=>Wb.call(e,t),pe=Array.isArray,$o=e=>oi(e)==="[object Map]",La=e=>oi(e)==="[object Set]",nf=e=>oi(e)==="[object Date]",ve=e=>typeof e=="function",Ae=e=>typeof e=="string",Tn=e=>typeof e=="symbol",Oe=e=>e!==null&&typeof e=="object",ia=e=>(Oe(e)||ve(e))&&ve(e.then)&&ve(e.catch),gh=Object.prototype.toString,oi=e=>gh.call(e),Ui=e=>oi(e).slice(8,-1),bh=e=>oi(e)==="[object Object]",Ma=e=>Ae(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ws=Ku(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),$a=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},Gb=/-\w/g,Mt=$a(e=>e.replace(Gb,t=>t.slice(1).toUpperCase())),Yb=/\B([A-Z])/g,hr=$a(e=>e.replace(Yb,"-$1").toLowerCase()),si=$a(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ki=$a(e=>e?`on${si(e)}`:""),jn=(e,t)=>!Object.is(e,t),qi=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},Gu=e=>{const t=parseFloat(e);return isNaN(t)?e:t},Jb=e=>{const t=Ae(e)?Number(e):NaN;return isNaN(t)?e:t};let rf;const ka=()=>rf||(rf=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ot(e){if(pe(e)){const t={};for(let n=0;n{if(n){const r=n.split(Zb);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function U(e){let t="";if(Ae(e))t=e;else if(pe(e))for(let n=0;nii(n,t))}const Eh=e=>!!(e&&e.__v_isRef===!0),He=e=>Ae(e)?e:e==null?"":pe(e)||Oe(e)&&(e.toString===gh||!ve(e.toString))?Eh(e)?He(e.value):JSON.stringify(e,_h,2):String(e),_h=(e,t)=>Eh(t)?_h(e,t.value):$o(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,o],s)=>(n[yl(r,s)+" =>"]=o,n),{})}:La(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>yl(n))}:Tn(t)?yl(t):Oe(t)&&!pe(t)&&!bh(t)?String(t):t,yl=(e,t="")=>{var n;return Tn(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};function oy(e){return e==null?"initial":typeof e=="string"?e===""?" ":e:String(e)}/** +* @vue/reactivity v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let xt;class Ch{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.__v_skip=!0,this.parent=xt,!t&&xt&&(this.index=(xt.scopes||(xt.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0&&(xt=this.prevScope,this.prevScope=void 0)}stop(t){if(this._active){this._active=!1;let n,r;for(n=0,r=this.effects.length;n0)return;if(Es){let t=Es;for(Es=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;Ss;){let t=Ss;for(Ss=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(r){e||(e=r)}t=n}}if(e)throw e}function xh(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Ih(e){let t,n=e.depsTail,r=n;for(;r;){const o=r.prevDep;r.version===-1?(r===n&&(n=o),Xu(r),sy(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=o}e.deps=t,e.depsTail=n}function eu(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Ph(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Ph(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Fs)||(e.globalVersion=Fs,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!eu(e))))return;e.flags|=2;const t=e.dep,n=et,r=Sn;et=e,Sn=!0;try{xh(e);const o=e.fn(e._value);(t.version===0||jn(o,e._value))&&(e.flags|=128,e._value=o,t.version++)}catch(o){throw t.version++,o}finally{et=n,Sn=r,Ih(e),e.flags&=-3}}function Xu(e,t=!1){const{dep:n,prevSub:r,nextSub:o}=e;if(r&&(r.nextSub=o,e.prevSub=void 0),o&&(o.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let s=n.computed.deps;s;s=s.nextDep)Xu(s,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function sy(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Sn=!0;const Nh=[];function lr(){Nh.push(Sn),Sn=!1}function ur(){const e=Nh.pop();Sn=e===void 0?!0:e}function of(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=et;et=void 0;try{t()}finally{et=n}}}let Fs=0,iy=class{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}};class Da{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!et||!Sn||et===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==et)n=this.activeLink=new iy(et,this),et.deps?(n.prevDep=et.depsTail,et.depsTail.nextDep=n,et.depsTail=n):et.deps=et.depsTail=n,Lh(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const r=n.nextDep;r.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=r),n.prevDep=et.depsTail,n.nextDep=void 0,et.depsTail.nextDep=n,et.depsTail=n,et.deps===n&&(et.deps=r)}return n}trigger(t){this.version++,Fs++,this.notify(t)}notify(t){Yu();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Ju()}}}function Lh(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let r=t.deps;r;r=r.nextDep)Lh(r)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const aa=new WeakMap,oo=Symbol(""),tu=Symbol(""),Bs=Symbol("");function It(e,t,n){if(Sn&&et){let r=aa.get(e);r||aa.set(e,r=new Map);let o=r.get(n);o||(r.set(n,o=new Da),o.map=r,o.key=n),o.track()}}function nr(e,t,n,r,o,s){const i=aa.get(e);if(!i){Fs++;return}const a=l=>{l&&l.trigger()};if(Yu(),t==="clear")i.forEach(a);else{const l=pe(e),u=l&&Ma(n);if(l&&n==="length"){const c=Number(r);i.forEach((f,d)=>{(d==="length"||d===Bs||!Tn(d)&&d>=c)&&a(f)})}else switch((n!==void 0||i.has(void 0))&&a(i.get(n)),u&&a(i.get(Bs)),t){case"add":l?u&&a(i.get("length")):(a(i.get(oo)),$o(e)&&a(i.get(tu)));break;case"delete":l||(a(i.get(oo)),$o(e)&&a(i.get(tu)));break;case"set":$o(e)&&a(i.get(oo));break}}Ju()}function ay(e,t){const n=aa.get(e);return n&&n.get(t)}function _o(e){const t=Me(e);return t===e?t:(It(t,"iterate",Bs),Qt(e)?t:t.map(On))}function Va(e){return It(e=Me(e),"iterate",Bs),e}function Dn(e,t){return cr(e)?Do(Hn(e)?On(t):t):On(t)}const ly={__proto__:null,[Symbol.iterator](){return Sl(this,Symbol.iterator,e=>Dn(this,e))},concat(...e){return _o(this).concat(...e.map(t=>pe(t)?_o(t):t))},entries(){return Sl(this,"entries",e=>(e[1]=Dn(this,e[1]),e))},every(e,t){return Yn(this,"every",e,t,void 0,arguments)},filter(e,t){return Yn(this,"filter",e,t,n=>n.map(r=>Dn(this,r)),arguments)},find(e,t){return Yn(this,"find",e,t,n=>Dn(this,n),arguments)},findIndex(e,t){return Yn(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Yn(this,"findLast",e,t,n=>Dn(this,n),arguments)},findLastIndex(e,t){return Yn(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Yn(this,"forEach",e,t,void 0,arguments)},includes(...e){return El(this,"includes",e)},indexOf(...e){return El(this,"indexOf",e)},join(e){return _o(this).join(e)},lastIndexOf(...e){return El(this,"lastIndexOf",e)},map(e,t){return Yn(this,"map",e,t,void 0,arguments)},pop(){return ls(this,"pop")},push(...e){return ls(this,"push",e)},reduce(e,...t){return sf(this,"reduce",e,t)},reduceRight(e,...t){return sf(this,"reduceRight",e,t)},shift(){return ls(this,"shift")},some(e,t){return Yn(this,"some",e,t,void 0,arguments)},splice(...e){return ls(this,"splice",e)},toReversed(){return _o(this).toReversed()},toSorted(e){return _o(this).toSorted(e)},toSpliced(...e){return _o(this).toSpliced(...e)},unshift(...e){return ls(this,"unshift",e)},values(){return Sl(this,"values",e=>Dn(this,e))}};function Sl(e,t,n){const r=Va(e),o=r[t]();return r!==e&&!Qt(e)&&(o._next=o.next,o.next=()=>{const s=o._next();return s.done||(s.value=n(s.value)),s}),o}const uy=Array.prototype;function Yn(e,t,n,r,o,s){const i=Va(e),a=i!==e&&!Qt(e),l=i[t];if(l!==uy[t]){const f=l.apply(e,s);return a?On(f):f}let u=n;i!==e&&(a?u=function(f,d){return n.call(this,Dn(e,f),d,e)}:n.length>2&&(u=function(f,d){return n.call(this,f,d,e)}));const c=l.call(i,u,r);return a&&o?o(c):c}function sf(e,t,n,r){const o=Va(e),s=o!==e&&!Qt(e);let i=n,a=!1;o!==e&&(s?(a=r.length===0,i=function(u,c,f){return a&&(a=!1,u=Dn(e,u)),n.call(this,u,Dn(e,c),f,e)}):n.length>3&&(i=function(u,c,f){return n.call(this,u,c,f,e)}));const l=o[t](i,...r);return a?Dn(e,l):l}function El(e,t,n){const r=Me(e);It(r,"iterate",Bs);const o=r[t](...n);return(o===-1||o===!1)&&ja(n[0])?(n[0]=Me(n[0]),r[t](...n)):o}function ls(e,t,n=[]){lr(),Yu();const r=Me(e)[t].apply(e,n);return Ju(),ur(),r}const cy=Ku("__proto__,__v_isRef,__isVue"),Mh=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Tn));function fy(e){Tn(e)||(e=String(e));const t=Me(this);return It(t,"has",e),t.hasOwnProperty(e)}class $h{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,r){if(n==="__v_skip")return t.__v_skip;const o=this._isReadonly,s=this._isShallow;if(n==="__v_isReactive")return!o;if(n==="__v_isReadonly")return o;if(n==="__v_isShallow")return s;if(n==="__v_raw")return r===(o?s?Sy:Dh:s?Bh:Fh).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(r)?t:void 0;const i=pe(t);if(!o){let l;if(i&&(l=ly[n]))return l;if(n==="hasOwnProperty")return fy}const a=Reflect.get(t,n,Ue(t)?t:r);if((Tn(n)?Mh.has(n):cy(n))||(o||It(t,"get",n),s))return a;if(Ue(a)){const l=i&&Ma(n)?a:a.value;return o&&Oe(l)?Mr(l):l}return Oe(a)?o?Mr(a):wt(a):a}}class kh extends $h{constructor(t=!1){super(!1,t)}set(t,n,r,o){let s=t[n];const i=pe(t)&&Ma(n);if(!this._isShallow){const u=cr(s);if(!Qt(r)&&!cr(r)&&(s=Me(s),r=Me(r)),!i&&Ue(s)&&!Ue(r))return u||(s.value=r),!0}const a=i?Number(n)e,Ci=e=>Reflect.getPrototypeOf(e);function my(e,t,n){return function(...r){const o=this.__v_raw,s=Me(o),i=$o(s),a=e==="entries"||e===Symbol.iterator&&i,l=e==="keys"&&i,u=o[e](...r),c=n?nu:t?Do:On;return!t&&It(s,"iterate",l?tu:oo),vt(Object.create(u),{next(){const{value:f,done:d}=u.next();return d?{value:f,done:d}:{value:a?[c(f[0]),c(f[1])]:c(f),done:d}}})}}function Ti(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function gy(e,t){const n={get(o){const s=this.__v_raw,i=Me(s),a=Me(o);e||(jn(o,a)&&It(i,"get",o),It(i,"get",a));const{has:l}=Ci(i),u=t?nu:e?Do:On;if(l.call(i,o))return u(s.get(o));if(l.call(i,a))return u(s.get(a));s!==i&&s.get(o)},get size(){const o=this.__v_raw;return!e&&It(Me(o),"iterate",oo),o.size},has(o){const s=this.__v_raw,i=Me(s),a=Me(o);return e||(jn(o,a)&&It(i,"has",o),It(i,"has",a)),o===a?s.has(o):s.has(o)||s.has(a)},forEach(o,s){const i=this,a=i.__v_raw,l=Me(a),u=t?nu:e?Do:On;return!e&&It(l,"iterate",oo),a.forEach((c,f)=>o.call(s,u(c),u(f),i))}};return vt(n,e?{add:Ti("add"),set:Ti("set"),delete:Ti("delete"),clear:Ti("clear")}:{add(o){const s=Me(this),i=Ci(s),a=Me(o),l=!t&&!Qt(o)&&!cr(o)?a:o;return i.has.call(s,l)||jn(o,l)&&i.has.call(s,o)||jn(a,l)&&i.has.call(s,a)||(s.add(l),nr(s,"add",l,l)),this},set(o,s){!t&&!Qt(s)&&!cr(s)&&(s=Me(s));const i=Me(this),{has:a,get:l}=Ci(i);let u=a.call(i,o);u||(o=Me(o),u=a.call(i,o));const c=l.call(i,o);return i.set(o,s),u?jn(s,c)&&nr(i,"set",o,s):nr(i,"add",o,s),this},delete(o){const s=Me(this),{has:i,get:a}=Ci(s);let l=i.call(s,o);l||(o=Me(o),l=i.call(s,o)),a&&a.call(s,o);const u=s.delete(o);return l&&nr(s,"delete",o,void 0),u},clear(){const o=Me(this),s=o.size!==0,i=o.clear();return s&&nr(o,"clear",void 0,void 0),i}}),["keys","values","entries",Symbol.iterator].forEach(o=>{n[o]=my(o,e,t)}),n}function Zu(e,t){const n=gy(e,t);return(r,o,s)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?r:Reflect.get(Ve(n,o)&&o in r?n:r,o,s)}const by={get:Zu(!1,!1)},yy={get:Zu(!1,!0)},wy={get:Zu(!0,!1)};const Fh=new WeakMap,Bh=new WeakMap,Dh=new WeakMap,Sy=new WeakMap;function Ey(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function _y(e){return e.__v_skip||!Object.isExtensible(e)?0:Ey(Ui(e))}function wt(e){return cr(e)?e:ec(e,!1,py,by,Fh)}function Qu(e){return ec(e,!1,vy,yy,Bh)}function Mr(e){return ec(e,!0,hy,wy,Dh)}function ec(e,t,n,r,o){if(!Oe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const s=_y(e);if(s===0)return e;const i=o.get(e);if(i)return i;const a=new Proxy(e,s===2?r:n);return o.set(e,a),a}function Hn(e){return cr(e)?Hn(e.__v_raw):!!(e&&e.__v_isReactive)}function cr(e){return!!(e&&e.__v_isReadonly)}function Qt(e){return!!(e&&e.__v_isShallow)}function ja(e){return e?!!e.__v_raw:!1}function Me(e){const t=e&&e.__v_raw;return t?Me(t):e}function Ds(e){return!Ve(e,"__v_skip")&&Object.isExtensible(e)&&yh(e,"__v_skip",!0),e}const On=e=>Oe(e)?wt(e):e,Do=e=>Oe(e)?Mr(e):e;function Ue(e){return e?e.__v_isRef===!0:!1}function V(e){return Vh(e,!1)}function ir(e){return Vh(e,!0)}function Vh(e,t){return Ue(e)?e:new Cy(e,t)}class Cy{constructor(t,n){this.dep=new Da,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:Me(t),this._value=n?t:On(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,r=this.__v_isShallow||Qt(t)||cr(t);t=r?t:Me(t),jn(t,n)&&(this._rawValue=t,this._value=r?t:On(t),this.dep.trigger())}}function g(e){return Ue(e)?e.value:e}const Ty={get:(e,t,n)=>t==="__v_raw"?e:g(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Ue(o)&&!Ue(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function jh(e){return Hn(e)?e:new Proxy(e,Ty)}class Oy{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new Da,{get:r,set:o}=t(n.track.bind(n),n.trigger.bind(n));this._get=r,this._set=o}get value(){return this._value=this._get()}set value(t){this._set(t)}}function Ay(e){return new Oy(e)}function vr(e){const t=pe(e)?new Array(e.length):{};for(const n in e)t[n]=zh(e,n);return t}class Ry{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0,this._value=void 0,this._raw=Me(t);let o=!0,s=t;if(!pe(t)||!Ma(String(n)))do o=!ja(s)||Qt(s);while(o&&(s=s.__v_raw));this._shallow=o}get value(){let t=this._object[this._key];return this._shallow&&(t=g(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&Ue(this._raw[this._key])){const n=this._object[this._key];if(Ue(n)){n.value=t;return}}this._object[this._key]=t}get dep(){return ay(this._raw,this._key)}}class xy{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Jt(e,t,n){return Ue(e)?e:ve(e)?new xy(e):Oe(e)&&arguments.length>1?zh(e,t,n):V(e)}function zh(e,t,n){return new Ry(e,t,n)}class Iy{constructor(t,n,r){this.fn=t,this.setter=n,this._value=void 0,this.dep=new Da(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Fs-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=r}notify(){if(this.flags|=16,!(this.flags&8)&&et!==this)return Rh(this,!0),!0}get value(){const t=this.dep.track();return Ph(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Py(e,t,n=!1){let r,o;return ve(e)?r=e:(r=e.get,o=e.set),new Iy(r,o,n)}const Oi={},la=new WeakMap;let Jr;function Ny(e,t=!1,n=Jr){if(n){let r=la.get(n);r||la.set(n,r=[]),r.push(e)}}function Ly(e,t,n=Ye){const{immediate:r,deep:o,once:s,scheduler:i,augmentJob:a,call:l}=n,u=w=>o?w:Qt(w)||o===!1||o===0?rr(w,1):rr(w);let c,f,d,v,p=!1,h=!1;if(Ue(e)?(f=()=>e.value,p=Qt(e)):Hn(e)?(f=()=>u(e),p=!0):pe(e)?(h=!0,p=e.some(w=>Hn(w)||Qt(w)),f=()=>e.map(w=>{if(Ue(w))return w.value;if(Hn(w))return u(w);if(ve(w))return l?l(w,2):w()})):ve(e)?t?f=l?()=>l(e,2):e:f=()=>{if(d){lr();try{d()}finally{ur()}}const w=Jr;Jr=c;try{return l?l(e,3,[v]):e(v)}finally{Jr=w}}:f=ht,t&&o){const w=f,y=o===!0?1/0:o;f=()=>rr(w(),y)}const b=Fa(),m=()=>{c.stop(),b&&b.active&&Wu(b.effects,c)};if(s&&t){const w=t;t=(...y)=>{w(...y),m()}}let S=h?new Array(e.length).fill(Oi):Oi;const _=w=>{if(!(!(c.flags&1)||!c.dirty&&!w))if(t){const y=c.run();if(o||p||(h?y.some((A,R)=>jn(A,S[R])):jn(y,S))){d&&d();const A=Jr;Jr=c;try{const R=[y,S===Oi?void 0:h&&S[0]===Oi?[]:S,v];S=y,l?l(t,3,R):t(...R)}finally{Jr=A}}}else c.run()};return a&&a(_),c=new Oh(f),c.scheduler=i?()=>i(_,!1):_,v=w=>Ny(w,!1,c),d=c.onStop=()=>{const w=la.get(c);if(w){if(l)l(w,4);else for(const y of w)y();la.delete(c)}},t?r?_(!0):S=c.run():i?i(_.bind(null,!0),!0):c.run(),m.pause=c.pause.bind(c),m.resume=c.resume.bind(c),m.stop=m,m}function rr(e,t=1/0,n){if(t<=0||!Oe(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,Ue(e))rr(e.value,t,n);else if(pe(e))for(let r=0;r{rr(r,t,n)});else if(bh(e)){for(const r in e)rr(e[r],t,n);for(const r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&rr(e[r],t,n)}return e}/** +* @vue/runtime-core v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function ai(e,t,n,r){try{return r?e(...r):e()}catch(o){za(o,t,n)}}function An(e,t,n,r){if(ve(e)){const o=ai(e,t,n,r);return o&&ia(o)&&o.catch(s=>{za(s,t,n)}),o}if(pe(e)){const o=[];for(let s=0;s>>1,o=Ft[r],s=Vs(o);s=Vs(n)?Ft.push(e):Ft.splice($y(t),0,e),e.flags|=1,Uh()}}function Uh(){ua||(ua=Hh.then(Wh))}function Kh(e){pe(e)?ko.push(...e):xr&&e.id===-1?xr.splice(xo+1,0,e):e.flags&1||(ko.push(e),e.flags|=1),Uh()}function af(e,t,n=kn+1){for(;nVs(n)-Vs(r));if(ko.length=0,xr){xr.push(...t);return}for(xr=t,xo=0;xoe.id==null?e.flags&2?-1:1/0:e.id;function Wh(e){try{for(kn=0;kn{r._d&&pa(-1);const s=ca(t);let i;try{i=e(...o)}finally{ca(s),r._d&&pa(1)}return i};return r._n=!0,r._c=!0,r._d=!0,r}function ct(e,t){if(Ct===null)return e;const n=Ga(Ct),r=e.dirs||(e.dirs=[]);for(let o=0;o1)return n&&ve(t)?t.call(r&&r.proxy):t}}function ky(){return!!(Ge()||so)}const Fy=Symbol.for("v-scx"),By=()=>Ee(Fy);function Ha(e,t){return nc(e,null,t)}function he(e,t,n){return nc(e,t,n)}function nc(e,t,n=Ye){const{immediate:r,deep:o,flush:s,once:i}=n,a=vt({},n),l=t&&r||!t&&s!=="post";let u;if(Hs){if(s==="sync"){const v=By();u=v.__watcherHandles||(v.__watcherHandles=[])}else if(!l){const v=()=>{};return v.stop=ht,v.resume=ht,v.pause=ht,v}}const c=Pt;a.call=(v,p,h)=>An(v,c,p,h);let f=!1;s==="post"?a.scheduler=v=>{Rt(v,c&&c.suspense)}:s!=="sync"&&(f=!0,a.scheduler=(v,p)=>{p?v():tc(v)}),a.augmentJob=v=>{t&&(v.flags|=4),f&&(v.flags|=2,c&&(v.id=c.uid,v.i=c))};const d=Ly(e,t,a);return Hs&&(u?u.push(d):l&&d()),d}function Dy(e,t,n){const r=this.proxy,o=Ae(e)?e.includes(".")?Yh(r,e):()=>r[e]:e.bind(r,r);let s;ve(t)?s=t:(s=t.handler,n=t);const i=li(this),a=nc(o,s.bind(r),n);return i(),a}function Yh(e,t){const n=t.split(".");return()=>{let r=e;for(let o=0;oe.__isTeleport,_s=e=>e&&(e.disabled||e.disabled===""),lf=e=>e&&(e.defer||e.defer===""),uf=e=>typeof SVGElement<"u"&&e instanceof SVGElement,cf=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,ru=(e,t)=>{const n=e&&e.to;return Ae(n)?t?t(n):null:n},Zh={name:"Teleport",__isTeleport:!0,process(e,t,n,r,o,s,i,a,l,u){const{mc:c,pc:f,pbc:d,o:{insert:v,querySelector:p,createText:h,createComment:b}}=u,m=_s(t.props);let{shapeFlag:S,children:_,dynamicChildren:w}=t;if(e==null){const y=t.el=h(""),A=t.anchor=h("");v(y,n,r),v(A,n,r);const R=(I,x)=>{S&16&&c(_,I,x,o,s,i,a,l)},N=()=>{const I=t.target=ru(t.props,p),x=ou(I,t,h,v);I&&(i!=="svg"&&uf(I)?i="svg":i!=="mathml"&&cf(I)&&(i="mathml"),o&&o.isCE&&(o.ce._teleportTargets||(o.ce._teleportTargets=new Set)).add(I),m||(R(I,x),Wi(t,!1)))};m&&(R(n,A),Wi(t,!0)),lf(t.props)?(t.el.__isMounted=!1,Rt(()=>{N(),delete t.el.__isMounted},s)):N()}else{if(lf(t.props)&&e.el.__isMounted===!1){Rt(()=>{Zh.process(e,t,n,r,o,s,i,a,l,u)},s);return}t.el=e.el,t.targetStart=e.targetStart;const y=t.anchor=e.anchor,A=t.target=e.target,R=t.targetAnchor=e.targetAnchor,N=_s(e.props),I=N?n:A,x=N?y:R;if(i==="svg"||uf(A)?i="svg":(i==="mathml"||cf(A))&&(i="mathml"),w?(d(e.dynamicChildren,w,I,o,s,i,a),cc(e,t,!0)):l||f(e,t,I,x,o,s,i,a,!1),m)N?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):Ai(t,n,y,u,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const k=t.target=ru(t.props,p);k&&Ai(t,k,null,u,0)}else N&&Ai(t,A,R,u,1);Wi(t,m)}},remove(e,t,n,{um:r,o:{remove:o}},s){const{shapeFlag:i,children:a,anchor:l,targetStart:u,targetAnchor:c,target:f,props:d}=e;if(f&&(o(u),o(c)),s&&o(l),i&16){const v=s||!_s(d);for(let p=0;p{e.isMounted=!0}),St(()=>{e.isUnmounting=!0}),e}const sn=[Function,Array],ev={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:sn,onEnter:sn,onAfterEnter:sn,onEnterCancelled:sn,onBeforeLeave:sn,onLeave:sn,onAfterLeave:sn,onLeaveCancelled:sn,onBeforeAppear:sn,onAppear:sn,onAfterAppear:sn,onAppearCancelled:sn},tv=e=>{const t=e.subTree;return t.component?tv(t.component):t},zy={name:"BaseTransition",props:ev,setup(e,{slots:t}){const n=Ge(),r=Qh();return()=>{const o=t.default&&rc(t.default(),!0);if(!o||!o.length)return;const s=nv(o),i=Me(e),{mode:a}=i;if(r.isLeaving)return _l(s);const l=ff(s);if(!l)return _l(s);let u=js(l,i,r,n,f=>u=f);l.type!==_t&&uo(l,u);let c=n.subTree&&ff(n.subTree);if(c&&c.type!==_t&&!Xr(c,l)&&tv(n).type!==_t){let f=js(c,i,r,n);if(uo(c,f),a==="out-in"&&l.type!==_t)return r.isLeaving=!0,f.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete f.afterLeave,c=void 0},_l(s);a==="in-out"&&l.type!==_t?f.delayLeave=(d,v,p)=>{const h=rv(r,c);h[String(c.key)]=c,d[Bn]=()=>{v(),d[Bn]=void 0,delete u.delayedLeave,c=void 0},u.delayedLeave=()=>{p(),delete u.delayedLeave,c=void 0}}:c=void 0}else c&&(c=void 0);return s}}};function nv(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==_t){t=n;break}}return t}const Hy=zy;function rv(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function js(e,t,n,r,o){const{appear:s,mode:i,persisted:a=!1,onBeforeEnter:l,onEnter:u,onAfterEnter:c,onEnterCancelled:f,onBeforeLeave:d,onLeave:v,onAfterLeave:p,onLeaveCancelled:h,onBeforeAppear:b,onAppear:m,onAfterAppear:S,onAppearCancelled:_}=t,w=String(e.key),y=rv(n,e),A=(I,x)=>{I&&An(I,r,9,x)},R=(I,x)=>{const k=x[1];A(I,x),pe(I)?I.every($=>$.length<=1)&&k():I.length<=1&&k()},N={mode:i,persisted:a,beforeEnter(I){let x=l;if(!n.isMounted)if(s)x=b||l;else return;I[Bn]&&I[Bn](!0);const k=y[w];k&&Xr(e,k)&&k.el[Bn]&&k.el[Bn](),A(x,[I])},enter(I){if(y[w]===e)return;let x=u,k=c,$=f;if(!n.isMounted)if(s)x=m||u,k=S||c,$=_||f;else return;let F=!1;I[us]=P=>{F||(F=!0,P?A($,[I]):A(k,[I]),N.delayedLeave&&N.delayedLeave(),I[us]=void 0)};const Y=I[us].bind(null,!1);x?R(x,[I,Y]):Y()},leave(I,x){const k=String(e.key);if(I[us]&&I[us](!0),n.isUnmounting)return x();A(d,[I]);let $=!1;I[Bn]=Y=>{$||($=!0,x(),Y?A(h,[I]):A(p,[I]),I[Bn]=void 0,y[k]===e&&delete y[k])};const F=I[Bn].bind(null,!1);y[k]=e,v?R(v,[I,F]):F()},clone(I){const x=js(I,t,n,r,o);return o&&o(x),x}};return N}function _l(e){if(Ua(e))return e=fr(e),e.children=null,e}function ff(e){if(!Ua(e))return Xh(e.type)&&e.children?nv(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&ve(n.default))return n.default()}}function uo(e,t){e.shapeFlag&6&&e.component?(e.transition=t,uo(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function rc(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;sCs(h,t&&(pe(t)?t[b]:t),n,r,o));return}if(Fo(r)&&!o){r.shapeFlag&512&&r.type.__asyncResolved&&r.component.subTree.component&&Cs(e,t,n,r.component.subTree);return}const s=r.shapeFlag&4?Ga(r.component):r.el,i=o?null:s,{i:a,r:l}=e,u=t&&t.r,c=a.refs===Ye?a.refs={}:a.refs,f=a.setupState,d=Me(f),v=f===Ye?mh:h=>df(c,h)?!1:Ve(d,h),p=(h,b)=>!(b&&df(c,b));if(u!=null&&u!==l){if(pf(t),Ae(u))c[u]=null,v(u)&&(f[u]=null);else if(Ue(u)){const h=t;p(u,h.k)&&(u.value=null),h.k&&(c[h.k]=null)}}if(ve(l))ai(l,a,12,[i,c]);else{const h=Ae(l),b=Ue(l);if(h||b){const m=()=>{if(e.f){const S=h?v(l)?f[l]:c[l]:p()||!e.k?l.value:c[e.k];if(o)pe(S)&&Wu(S,s);else if(pe(S))S.includes(s)||S.push(s);else if(h)c[l]=[s],v(l)&&(f[l]=c[l]);else{const _=[s];p(l,e.k)&&(l.value=_),e.k&&(c[e.k]=_)}}else h?(c[l]=i,v(l)&&(f[l]=i)):b&&(p(l,e.k)&&(l.value=i),e.k&&(c[e.k]=i))};if(i){const S=()=>{m(),fa.delete(e)};S.id=-1,fa.set(e,S),Rt(S,n)}else pf(e),m()}}}function pf(e){const t=fa.get(e);t&&(t.flags|=8,fa.delete(e))}ka().requestIdleCallback;ka().cancelIdleCallback;const Fo=e=>!!e.type.__asyncLoader,Ua=e=>e.type.__isKeepAlive;function Ka(e,t){sv(e,"a",t)}function oc(e,t){sv(e,"da",t)}function sv(e,t,n=Pt){const r=e.__wdc||(e.__wdc=()=>{let o=n;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(qa(t,r,n),n){let o=n.parent;for(;o&&o.parent;)Ua(o.parent.vnode)&&Uy(r,t,n,o),o=o.parent}}function Uy(e,t,n,r){const o=qa(t,e,r,!0);kr(()=>{Wu(r[t],o)},n)}function qa(e,t,n=Pt,r=!1){if(n){const o=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...i)=>{lr();const a=li(n),l=An(t,n,e,i);return a(),ur(),l});return r?o.unshift(s):o.push(s),s}}const mr=e=>(t,n=Pt)=>{(!Hs||e==="sp")&&qa(e,(...r)=>t(...r),n)},sc=mr("bm"),Ke=mr("m"),iv=mr("bu"),mo=mr("u"),St=mr("bum"),kr=mr("um"),Ky=mr("sp"),qy=mr("rtg"),Wy=mr("rtc");function Gy(e,t=Pt){qa("ec",e,t)}const ic="components",Yy="directives";function Yt(e,t){return ac(ic,e,!0,t)||e}const av=Symbol.for("v-ndc");function Xe(e){return Ae(e)?ac(ic,e,!1)||e:e||av}function Jy(e){return ac(Yy,e)}function ac(e,t,n=!0,r=!1){const o=Ct||Pt;if(o){const s=o.type;if(e===ic){const a=L0(s,!1);if(a&&(a===t||a===Mt(t)||a===si(Mt(t))))return s}const i=hf(o[e]||s[e],t)||hf(o.appContext[e],t);return!i&&r?s:i}}function hf(e,t){return e&&(e[t]||e[Mt(t)]||e[si(Mt(t))])}function vf(e,t,n,r){let o;const s=n,i=pe(e);if(i||Ae(e)){const a=i&&Hn(e);let l=!1,u=!1;a&&(l=!Qt(e),u=cr(e),e=Va(e)),o=new Array(e.length);for(let c=0,f=e.length;ct(a,l,void 0,s));else{const a=Object.keys(e);o=new Array(a.length);for(let l=0,u=a.length;l{const s=r.fn(...o);return s&&(s.key=r.key),s}:r.fn)}return e}function de(e,t,n={},r,o){if(Ct.ce||Ct.parent&&Fo(Ct.parent)&&Ct.parent.ce){const u=Object.keys(n).length>0;return t!=="default"&&(n.name=t),L(),fe(nt,null,[re("slot",n,r&&r())],u?-2:64)}let s=e[t];s&&s._c&&(s._d=!1),L();const i=s&&uv(s(n)),a=n.key||i&&i.key,l=fe(nt,{key:(a&&!Tn(a)?a:`_${t}`)+(!i&&r?"_fb":"")},i||(r?r():[]),i&&e._===1?64:-2);return l.scopeId&&(l.slotScopeIds=[l.scopeId+"-s"]),s&&s._c&&(s._d=!0),l}function uv(e){return e.some(t=>cn(t)?!(t.type===_t||t.type===nt&&!uv(t.children)):!0)?e:null}const su=e=>e?xv(e)?Ga(e):su(e.parent):null,Ts=vt(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>su(e.parent),$root:e=>su(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>dv(e),$forceUpdate:e=>e.f||(e.f=()=>{tc(e.update)}),$nextTick:e=>e.n||(e.n=Ie.bind(e.proxy)),$watch:e=>Dy.bind(e)}),Cl=(e,t)=>e!==Ye&&!e.__isScriptSetup&&Ve(e,t),Xy={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:r,data:o,props:s,accessCache:i,type:a,appContext:l}=e;if(t[0]!=="$"){const d=i[t];if(d!==void 0)switch(d){case 1:return r[t];case 2:return o[t];case 4:return n[t];case 3:return s[t]}else{if(Cl(r,t))return i[t]=1,r[t];if(o!==Ye&&Ve(o,t))return i[t]=2,o[t];if(Ve(s,t))return i[t]=3,s[t];if(n!==Ye&&Ve(n,t))return i[t]=4,n[t];iu&&(i[t]=0)}}const u=Ts[t];let c,f;if(u)return t==="$attrs"&&It(e.attrs,"get",""),u(e);if((c=a.__cssModules)&&(c=c[t]))return c;if(n!==Ye&&Ve(n,t))return i[t]=4,n[t];if(f=l.config.globalProperties,Ve(f,t))return f[t]},set({_:e},t,n){const{data:r,setupState:o,ctx:s}=e;return Cl(o,t)?(o[t]=n,!0):r!==Ye&&Ve(r,t)?(r[t]=n,!0):Ve(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(s[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:o,props:s,type:i}},a){let l;return!!(n[a]||e!==Ye&&a[0]!=="$"&&Ve(e,a)||Cl(t,a)||Ve(s,a)||Ve(r,a)||Ve(Ts,a)||Ve(o.config.globalProperties,a)||(l=i.__cssModules)&&l[a])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Ve(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function go(){return cv().slots}function Zy(){return cv().attrs}function cv(e){const t=Ge();return t.setupContext||(t.setupContext=Pv(t))}function mf(e){return pe(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let iu=!0;function Qy(e){const t=dv(e),n=e.proxy,r=e.ctx;iu=!1,t.beforeCreate&&gf(t.beforeCreate,e,"bc");const{data:o,computed:s,methods:i,watch:a,provide:l,inject:u,created:c,beforeMount:f,mounted:d,beforeUpdate:v,updated:p,activated:h,deactivated:b,beforeDestroy:m,beforeUnmount:S,destroyed:_,unmounted:w,render:y,renderTracked:A,renderTriggered:R,errorCaptured:N,serverPrefetch:I,expose:x,inheritAttrs:k,components:$,directives:F,filters:Y}=t;if(u&&e0(u,r,null),i)for(const j in i){const Q=i[j];ve(Q)&&(r[j]=Q.bind(n))}if(o){const j=o.call(n,n);Oe(j)&&(e.data=wt(j))}if(iu=!0,s)for(const j in s){const Q=s[j],me=ve(Q)?Q.bind(n,n):ve(Q.get)?Q.get.bind(n,n):ht,Pe=!ve(Q)&&ve(Q.set)?Q.set.bind(n):ht,Re=C({get:me,set:Pe});Object.defineProperty(r,j,{enumerable:!0,configurable:!0,get:()=>Re.value,set:Ce=>Re.value=Ce})}if(a)for(const j in a)fv(a[j],r,n,j);if(l){const j=ve(l)?l.call(n):l;Reflect.ownKeys(j).forEach(Q=>{ft(Q,j[Q])})}c&&gf(c,e,"c");function O(j,Q){pe(Q)?Q.forEach(me=>j(me.bind(n))):Q&&j(Q.bind(n))}if(O(sc,f),O(Ke,d),O(iv,v),O(mo,p),O(Ka,h),O(oc,b),O(Gy,N),O(Wy,A),O(qy,R),O(St,S),O(kr,w),O(Ky,I),pe(x))if(x.length){const j=e.exposed||(e.exposed={});x.forEach(Q=>{Object.defineProperty(j,Q,{get:()=>n[Q],set:me=>n[Q]=me,enumerable:!0})})}else e.exposed||(e.exposed={});y&&e.render===ht&&(e.render=y),k!=null&&(e.inheritAttrs=k),$&&(e.components=$),F&&(e.directives=F),I&&ov(e)}function e0(e,t,n=ht){pe(e)&&(e=au(e));for(const r in e){const o=e[r];let s;Oe(o)?"default"in o?s=Ee(o.from||r,o.default,!0):s=Ee(o.from||r):s=Ee(o),Ue(s)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>s.value,set:i=>s.value=i}):t[r]=s}}function gf(e,t,n){An(pe(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function fv(e,t,n,r){let o=r.includes(".")?Yh(n,r):()=>n[r];if(Ae(e)){const s=t[e];ve(s)&&he(o,s)}else if(ve(e))he(o,e.bind(n));else if(Oe(e))if(pe(e))e.forEach(s=>fv(s,t,n,r));else{const s=ve(e.handler)?e.handler.bind(n):t[e.handler];ve(s)&&he(o,s,e)}}function dv(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:o,optionsCache:s,config:{optionMergeStrategies:i}}=e.appContext,a=s.get(t);let l;return a?l=a:!o.length&&!n&&!r?l=t:(l={},o.length&&o.forEach(u=>da(l,u,i,!0)),da(l,t,i)),Oe(t)&&s.set(t,l),l}function da(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&da(e,s,n,!0),o&&o.forEach(i=>da(e,i,n,!0));for(const i in t)if(!(r&&i==="expose")){const a=t0[i]||n&&n[i];e[i]=a?a(e[i],t[i]):t[i]}return e}const t0={data:bf,props:yf,emits:yf,methods:gs,computed:gs,beforeCreate:$t,created:$t,beforeMount:$t,mounted:$t,beforeUpdate:$t,updated:$t,beforeDestroy:$t,beforeUnmount:$t,destroyed:$t,unmounted:$t,activated:$t,deactivated:$t,errorCaptured:$t,serverPrefetch:$t,components:gs,directives:gs,watch:r0,provide:bf,inject:n0};function bf(e,t){return t?e?function(){return vt(ve(e)?e.call(this,this):e,ve(t)?t.call(this,this):t)}:t:e}function n0(e,t){return gs(au(e),au(t))}function au(e){if(pe(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Mt(t)}Modifiers`]||e[`${hr(t)}Modifiers`];function a0(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||Ye;let o=n;const s=t.startsWith("update:"),i=s&&i0(r,t.slice(7));i&&(i.trim&&(o=n.map(c=>Ae(c)?c.trim():c)),i.number&&(o=n.map(Gu)));let a,l=r[a=Ki(t)]||r[a=Ki(Mt(t))];!l&&s&&(l=r[a=Ki(hr(t))]),l&&An(l,e,6,o);const u=r[a+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[a])return;e.emitted[a]=!0,An(u,e,6,o)}}const l0=new WeakMap;function hv(e,t,n=!1){const r=n?l0:t.emitsCache,o=r.get(e);if(o!==void 0)return o;const s=e.emits;let i={},a=!1;if(!ve(e)){const l=u=>{const c=hv(u,t,!0);c&&(a=!0,vt(i,c))};!n&&t.mixins.length&&t.mixins.forEach(l),e.extends&&l(e.extends),e.mixins&&e.mixins.forEach(l)}return!s&&!a?(Oe(e)&&r.set(e,null),null):(pe(s)?s.forEach(l=>i[l]=null):vt(i,s),Oe(e)&&r.set(e,i),i)}function Wa(e,t){return!e||!Na(t)?!1:(t=t.slice(2).replace(/Once$/,""),Ve(e,t[0].toLowerCase()+t.slice(1))||Ve(e,hr(t))||Ve(e,t))}function wf(e){const{type:t,vnode:n,proxy:r,withProxy:o,propsOptions:[s],slots:i,attrs:a,emit:l,render:u,renderCache:c,props:f,data:d,setupState:v,ctx:p,inheritAttrs:h}=e,b=ca(e);let m,S;try{if(n.shapeFlag&4){const w=o||r,y=w;m=Vn(u.call(y,w,c,f,v,d,p)),S=a}else{const w=t;m=Vn(w.length>1?w(f,{attrs:a,slots:i,emit:l}):w(f,null)),S=t.props?a:u0(a)}}catch(w){Os.length=0,za(w,e,1),m=re(_t)}let _=m;if(S&&h!==!1){const w=Object.keys(S),{shapeFlag:y}=_;w.length&&y&7&&(s&&w.some(qu)&&(S=c0(S,s)),_=fr(_,S,!1,!0))}return n.dirs&&(_=fr(_,null,!1,!0),_.dirs=_.dirs?_.dirs.concat(n.dirs):n.dirs),n.transition&&uo(_,n.transition),m=_,ca(b),m}const u0=e=>{let t;for(const n in e)(n==="class"||n==="style"||Na(n))&&((t||(t={}))[n]=e[n]);return t},c0=(e,t)=>{const n={};for(const r in e)(!qu(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function f0(e,t,n){const{props:r,children:o,component:s}=e,{props:i,children:a,patchFlag:l}=t,u=s.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&l>=0){if(l&1024)return!0;if(l&16)return r?Sf(r,i,u):!!i;if(l&8){const c=t.dynamicProps;for(let f=0;fObject.create(mv),bv=e=>Object.getPrototypeOf(e)===mv;function p0(e,t,n,r=!1){const o={},s=gv();e.propsDefaults=Object.create(null),yv(e,t,o,s);for(const i in e.propsOptions[0])i in o||(o[i]=void 0);n?e.props=r?o:Qu(o):e.type.props?e.props=o:e.props=s,e.attrs=s}function h0(e,t,n,r){const{props:o,attrs:s,vnode:{patchFlag:i}}=e,a=Me(o),[l]=e.propsOptions;let u=!1;if((r||i>0)&&!(i&16)){if(i&8){const c=e.vnode.dynamicProps;for(let f=0;f{l=!0;const[d,v]=wv(f,t,!0);vt(i,d),v&&a.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}if(!s&&!l)return Oe(e)&&r.set(e,Mo),Mo;if(pe(s))for(let c=0;ce==="_"||e==="_ctx"||e==="$stable",uc=e=>pe(e)?e.map(Vn):[Vn(e)],m0=(e,t,n)=>{if(t._n)return t;const r=ce((...o)=>uc(t(...o)),n);return r._c=!1,r},Sv=(e,t,n)=>{const r=e._ctx;for(const o in e){if(lc(o))continue;const s=e[o];if(ve(s))t[o]=m0(o,s,r);else if(s!=null){const i=uc(s);t[o]=()=>i}}},Ev=(e,t)=>{const n=uc(t);e.slots.default=()=>n},_v=(e,t,n)=>{for(const r in t)(n||!lc(r))&&(e[r]=t[r])},g0=(e,t,n)=>{const r=e.slots=gv();if(e.vnode.shapeFlag&32){const o=t._;o?(_v(r,t,n),n&&yh(r,"_",o,!0)):Sv(t,r)}else t&&Ev(e,t)},b0=(e,t,n)=>{const{vnode:r,slots:o}=e;let s=!0,i=Ye;if(r.shapeFlag&32){const a=t._;a?n&&a===1?s=!1:_v(o,t,n):(s=!t.$stable,Sv(t,o)),i=t}else t&&(Ev(e,t),i={default:1});if(s)for(const a in o)!lc(a)&&i[a]==null&&delete o[a]},Rt=_0;function y0(e){return w0(e)}function w0(e,t){const n=ka();n.__VUE__=!0;const{insert:r,remove:o,patchProp:s,createElement:i,createText:a,createComment:l,setText:u,setElementText:c,parentNode:f,nextSibling:d,setScopeId:v=ht,insertStaticContent:p}=e,h=(E,T,M,W=null,G=null,q=null,se=void 0,ne=null,te=!!T.dynamicChildren)=>{if(E===T)return;E&&!Xr(E,T)&&(W=B(E),Ce(E,G,q,!0),E=null),T.patchFlag===-2&&(te=!1,T.dynamicChildren=null);const{type:X,ref:Se,shapeFlag:ue}=T;switch(X){case Zo:b(E,T,M,W);break;case _t:m(E,T,M,W);break;case Gi:E==null&&S(T,M,W,se);break;case nt:$(E,T,M,W,G,q,se,ne,te);break;default:ue&1?y(E,T,M,W,G,q,se,ne,te):ue&6?F(E,T,M,W,G,q,se,ne,te):(ue&64||ue&128)&&X.process(E,T,M,W,G,q,se,ne,te,oe)}Se!=null&&G?Cs(Se,E&&E.ref,q,T||E,!T):Se==null&&E&&E.ref!=null&&Cs(E.ref,null,q,E,!0)},b=(E,T,M,W)=>{if(E==null)r(T.el=a(T.children),M,W);else{const G=T.el=E.el;T.children!==E.children&&u(G,T.children)}},m=(E,T,M,W)=>{E==null?r(T.el=l(T.children||""),M,W):T.el=E.el},S=(E,T,M,W)=>{[E.el,E.anchor]=p(E.children,T,M,W,E.el,E.anchor)},_=({el:E,anchor:T},M,W)=>{let G;for(;E&&E!==T;)G=d(E),r(E,M,W),E=G;r(T,M,W)},w=({el:E,anchor:T})=>{let M;for(;E&&E!==T;)M=d(E),o(E),E=M;o(T)},y=(E,T,M,W,G,q,se,ne,te)=>{if(T.type==="svg"?se="svg":T.type==="math"&&(se="mathml"),E==null)A(T,M,W,G,q,se,ne,te);else{const X=E.el&&E.el._isVueCE?E.el:null;try{X&&X._beginPatch(),I(E,T,G,q,se,ne,te)}finally{X&&X._endPatch()}}},A=(E,T,M,W,G,q,se,ne)=>{let te,X;const{props:Se,shapeFlag:ue,transition:ye,dirs:z}=E;if(te=E.el=i(E.type,q,Se&&Se.is,Se),ue&8?c(te,E.children):ue&16&&N(E.children,te,null,W,G,Tl(E,q),se,ne),z&&Ur(E,null,W,"created"),R(te,E,E.scopeId,se,W),Se){for(const Le in Se)Le!=="value"&&!ws(Le)&&s(te,Le,null,Se[Le],q,W);"value"in Se&&s(te,"value",null,Se.value,q),(X=Se.onVnodeBeforeMount)&&Mn(X,W,E)}z&&Ur(E,null,W,"beforeMount");const be=S0(G,ye);be&&ye.beforeEnter(te),r(te,T,M),((X=Se&&Se.onVnodeMounted)||be||z)&&Rt(()=>{X&&Mn(X,W,E),be&&ye.enter(te),z&&Ur(E,null,W,"mounted")},G)},R=(E,T,M,W,G)=>{if(M&&v(E,M),W)for(let q=0;q{for(let X=te;X{const ne=T.el=E.el;let{patchFlag:te,dynamicChildren:X,dirs:Se}=T;te|=E.patchFlag&16;const ue=E.props||Ye,ye=T.props||Ye;let z;if(M&&Kr(M,!1),(z=ye.onVnodeBeforeUpdate)&&Mn(z,M,T,E),Se&&Ur(T,E,M,"beforeUpdate"),M&&Kr(M,!0),(ue.innerHTML&&ye.innerHTML==null||ue.textContent&&ye.textContent==null)&&c(ne,""),X?x(E.dynamicChildren,X,ne,M,W,Tl(T,G),q):se||Q(E,T,ne,null,M,W,Tl(T,G),q,!1),te>0){if(te&16)k(ne,ue,ye,M,G);else if(te&2&&ue.class!==ye.class&&s(ne,"class",null,ye.class,G),te&4&&s(ne,"style",ue.style,ye.style,G),te&8){const be=T.dynamicProps;for(let Le=0;Le{z&&Mn(z,M,T,E),Se&&Ur(T,E,M,"updated")},W)},x=(E,T,M,W,G,q,se)=>{for(let ne=0;ne{if(T!==M){if(T!==Ye)for(const q in T)!ws(q)&&!(q in M)&&s(E,q,T[q],null,G,W);for(const q in M){if(ws(q))continue;const se=M[q],ne=T[q];se!==ne&&q!=="value"&&s(E,q,ne,se,G,W)}"value"in M&&s(E,"value",T.value,M.value,G)}},$=(E,T,M,W,G,q,se,ne,te)=>{const X=T.el=E?E.el:a(""),Se=T.anchor=E?E.anchor:a("");let{patchFlag:ue,dynamicChildren:ye,slotScopeIds:z}=T;z&&(ne=ne?ne.concat(z):z),E==null?(r(X,M,W),r(Se,M,W),N(T.children||[],M,Se,G,q,se,ne,te)):ue>0&&ue&64&&ye&&E.dynamicChildren&&E.dynamicChildren.length===ye.length?(x(E.dynamicChildren,ye,M,G,q,se,ne),(T.key!=null||G&&T===G.subTree)&&cc(E,T,!0)):Q(E,T,M,Se,G,q,se,ne,te)},F=(E,T,M,W,G,q,se,ne,te)=>{T.slotScopeIds=ne,E==null?T.shapeFlag&512?G.ctx.activate(T,M,W,se,te):Y(T,M,W,G,q,se,te):P(E,T,te)},Y=(E,T,M,W,G,q,se)=>{const ne=E.component=x0(E,W,G);if(Ua(E)&&(ne.ctx.renderer=oe),I0(ne,!1,se),ne.asyncDep){if(G&&G.registerDep(ne,O,se),!E.el){const te=ne.subTree=re(_t);m(null,te,T,M),E.placeholder=te.el}}else O(ne,E,T,M,G,q,se)},P=(E,T,M)=>{const W=T.component=E.component;if(f0(E,T,M))if(W.asyncDep&&!W.asyncResolved){j(W,T,M);return}else W.next=T,W.update();else T.el=E.el,W.vnode=T},O=(E,T,M,W,G,q,se)=>{const ne=()=>{if(E.isMounted){let{next:ue,bu:ye,u:z,parent:be,vnode:Le}=E;{const on=Cv(E);if(on){ue&&(ue.el=Le.el,j(E,ue,se)),on.asyncDep.then(()=>{Rt(()=>{E.isUnmounted||X()},G)});return}}let ke=ue,dt;Kr(E,!1),ue?(ue.el=Le.el,j(E,ue,se)):ue=Le,ye&&qi(ye),(dt=ue.props&&ue.props.onVnodeBeforeUpdate)&&Mn(dt,be,ue,Le),Kr(E,!0);const pt=wf(E),rn=E.subTree;E.subTree=pt,h(rn,pt,f(rn.el),B(rn),E,G,q),ue.el=pt.el,ke===null&&d0(E,pt.el),z&&Rt(z,G),(dt=ue.props&&ue.props.onVnodeUpdated)&&Rt(()=>Mn(dt,be,ue,Le),G)}else{let ue;const{el:ye,props:z}=T,{bm:be,m:Le,parent:ke,root:dt,type:pt}=E,rn=Fo(T);Kr(E,!1),be&&qi(be),!rn&&(ue=z&&z.onVnodeBeforeMount)&&Mn(ue,ke,T),Kr(E,!0);{dt.ce&&dt.ce._hasShadowRoot()&&dt.ce._injectChildStyle(pt,E.parent?E.parent.type:void 0);const on=E.subTree=wf(E);h(null,on,M,W,E,G,q),T.el=on.el}if(Le&&Rt(Le,G),!rn&&(ue=z&&z.onVnodeMounted)){const on=T;Rt(()=>Mn(ue,ke,on),G)}(T.shapeFlag&256||ke&&Fo(ke.vnode)&&ke.vnode.shapeFlag&256)&&E.a&&Rt(E.a,G),E.isMounted=!0,T=M=W=null}};E.scope.on();const te=E.effect=new Oh(ne);E.scope.off();const X=E.update=te.run.bind(te),Se=E.job=te.runIfDirty.bind(te);Se.i=E,Se.id=E.uid,te.scheduler=()=>tc(Se),Kr(E,!0),X()},j=(E,T,M)=>{T.component=E;const W=E.vnode.props;E.vnode=T,E.next=null,h0(E,T.props,W,M),b0(E,T.children,M),lr(),af(E),ur()},Q=(E,T,M,W,G,q,se,ne,te=!1)=>{const X=E&&E.children,Se=E?E.shapeFlag:0,ue=T.children,{patchFlag:ye,shapeFlag:z}=T;if(ye>0){if(ye&128){Pe(X,ue,M,W,G,q,se,ne,te);return}else if(ye&256){me(X,ue,M,W,G,q,se,ne,te);return}}z&8?(Se&16&&De(X,G,q),ue!==X&&c(M,ue)):Se&16?z&16?Pe(X,ue,M,W,G,q,se,ne,te):De(X,G,q,!0):(Se&8&&c(M,""),z&16&&N(ue,M,W,G,q,se,ne,te))},me=(E,T,M,W,G,q,se,ne,te)=>{E=E||Mo,T=T||Mo;const X=E.length,Se=T.length,ue=Math.min(X,Se);let ye;for(ye=0;yeSe?De(E,G,q,!0,!1,ue):N(T,M,W,G,q,se,ne,te,ue)},Pe=(E,T,M,W,G,q,se,ne,te)=>{let X=0;const Se=T.length;let ue=E.length-1,ye=Se-1;for(;X<=ue&&X<=ye;){const z=E[X],be=T[X]=te?tr(T[X]):Vn(T[X]);if(Xr(z,be))h(z,be,M,null,G,q,se,ne,te);else break;X++}for(;X<=ue&&X<=ye;){const z=E[ue],be=T[ye]=te?tr(T[ye]):Vn(T[ye]);if(Xr(z,be))h(z,be,M,null,G,q,se,ne,te);else break;ue--,ye--}if(X>ue){if(X<=ye){const z=ye+1,be=zye)for(;X<=ue;)Ce(E[X],G,q,!0),X++;else{const z=X,be=X,Le=new Map;for(X=be;X<=ye;X++){const Ot=T[X]=te?tr(T[X]):Vn(T[X]);Ot.key!=null&&Le.set(Ot.key,X)}let ke,dt=0;const pt=ye-be+1;let rn=!1,on=0;const jr=new Array(pt);for(X=0;X=pt){Ce(Ot,G,q,!0);continue}let qt;if(Ot.key!=null)qt=Le.get(Ot.key);else for(ke=be;ke<=ye;ke++)if(jr[ke-be]===0&&Xr(Ot,T[ke])){qt=ke;break}qt===void 0?Ce(Ot,G,q,!0):(jr[qt-be]=X+1,qt>=on?on=qt:rn=!0,h(Ot,T[qt],M,null,G,q,se,ne,te),dt++)}const as=rn?E0(jr):Mo;for(ke=as.length-1,X=pt-1;X>=0;X--){const Ot=be+X,qt=T[Ot],zr=T[Ot+1],Si=Ot+1{const{el:q,type:se,transition:ne,children:te,shapeFlag:X}=E;if(X&6){Re(E.component.subTree,T,M,W);return}if(X&128){E.suspense.move(T,M,W);return}if(X&64){se.move(E,T,M,oe);return}if(se===nt){r(q,T,M);for(let ue=0;uene.enter(q),G);else{const{leave:ue,delayLeave:ye,afterLeave:z}=ne,be=()=>{E.ctx.isUnmounted?o(q):r(q,T,M)},Le=()=>{q._isLeaving&&q[Bn](!0),ue(q,()=>{be(),z&&z()})};ye?ye(q,be,Le):Le()}else r(q,T,M)},Ce=(E,T,M,W=!1,G=!1)=>{const{type:q,props:se,ref:ne,children:te,dynamicChildren:X,shapeFlag:Se,patchFlag:ue,dirs:ye,cacheIndex:z}=E;if(ue===-2&&(G=!1),ne!=null&&(lr(),Cs(ne,null,M,E,!0),ur()),z!=null&&(T.renderCache[z]=void 0),Se&256){T.ctx.deactivate(E);return}const be=Se&1&&ye,Le=!Fo(E);let ke;if(Le&&(ke=se&&se.onVnodeBeforeUnmount)&&Mn(ke,T,E),Se&6)ze(E.component,M,W);else{if(Se&128){E.suspense.unmount(M,W);return}be&&Ur(E,null,T,"beforeUnmount"),Se&64?E.type.remove(E,T,M,oe,W):X&&!X.hasOnce&&(q!==nt||ue>0&&ue&64)?De(X,T,M,!1,!0):(q===nt&&ue&384||!G&&Se&16)&&De(te,T,M),W&&_e(E)}(Le&&(ke=se&&se.onVnodeUnmounted)||be)&&Rt(()=>{ke&&Mn(ke,T,E),be&&Ur(E,null,T,"unmounted")},M)},_e=E=>{const{type:T,el:M,anchor:W,transition:G}=E;if(T===nt){qe(M,W);return}if(T===Gi){w(E);return}const q=()=>{o(M),G&&!G.persisted&&G.afterLeave&&G.afterLeave()};if(E.shapeFlag&1&&G&&!G.persisted){const{leave:se,delayLeave:ne}=G,te=()=>se(M,q);ne?ne(E.el,q,te):te()}else q()},qe=(E,T)=>{let M;for(;E!==T;)M=d(E),o(E),E=M;o(T)},ze=(E,T,M)=>{const{bum:W,scope:G,job:q,subTree:se,um:ne,m:te,a:X}=E;_f(te),_f(X),W&&qi(W),G.stop(),q&&(q.flags|=8,Ce(se,E,T,M)),ne&&Rt(ne,T),Rt(()=>{E.isUnmounted=!0},T)},De=(E,T,M,W=!1,G=!1,q=0)=>{for(let se=q;se{if(E.shapeFlag&6)return B(E.component.subTree);if(E.shapeFlag&128)return E.suspense.next();const T=d(E.anchor||E.el),M=T&&T[Jh];return M?d(M):T};let K=!1;const J=(E,T,M)=>{let W;E==null?T._vnode&&(Ce(T._vnode,null,null,!0),W=T._vnode.component):h(T._vnode||null,E,T,null,null,null,M),T._vnode=E,K||(K=!0,af(W),qh(),K=!1)},oe={p:h,um:Ce,m:Re,r:_e,mt:Y,mc:N,pc:Q,pbc:x,n:B,o:e};return{render:J,hydrate:void 0,createApp:s0(J)}}function Tl({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Kr({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function S0(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function cc(e,t,n=!1){const r=e.children,o=t.children;if(pe(r)&&pe(o))for(let s=0;s>1,e[n[a]]0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,i=n[s-1];s-- >0;)n[s]=i,i=t[i];return n}function Cv(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Cv(t)}function _f(e){if(e)for(let t=0;te.__isSuspense;function _0(e,t){t&&t.pendingBranch?pe(e)?t.effects.push(...e):t.effects.push(e):Kh(e)}const nt=Symbol.for("v-fgt"),Zo=Symbol.for("v-txt"),_t=Symbol.for("v-cmt"),Gi=Symbol.for("v-stc"),Os=[];let Xt=null;function L(e=!1){Os.push(Xt=e?null:[])}function C0(){Os.pop(),Xt=Os[Os.length-1]||null}let zs=1;function pa(e,t=!1){zs+=e,e<0&&Xt&&t&&(Xt.hasOnce=!0)}function Av(e){return e.dynamicChildren=zs>0?Xt||Mo:null,C0(),zs>0&&Xt&&Xt.push(e),e}function ee(e,t,n,r,o,s){return Av(le(e,t,n,r,o,s,!0))}function fe(e,t,n,r,o){return Av(re(e,t,n,r,o,!0))}function cn(e){return e?e.__v_isVNode===!0:!1}function Xr(e,t){return e.type===t.type&&e.key===t.key}const Rv=({key:e})=>e??null,Yi=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?Ae(e)||Ue(e)||ve(e)?{i:Ct,r:e,k:t,f:!!n}:e:null);function le(e,t=null,n=null,r=0,o=null,s=e===nt?0:1,i=!1,a=!1){const l={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Rv(t),ref:t&&Yi(t),scopeId:Gh,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Ct};return a?(fc(l,n),s&128&&e.normalize(l)):n&&(l.shapeFlag|=Ae(n)?8:16),zs>0&&!i&&Xt&&(l.patchFlag>0||s&6)&&l.patchFlag!==32&&Xt.push(l),l}const re=T0;function T0(e,t=null,n=null,r=0,o=null,s=!1){if((!e||e===av)&&(e=_t),cn(e)){const a=fr(e,t,!0);return n&&fc(a,n),zs>0&&!s&&Xt&&(a.shapeFlag&6?Xt[Xt.indexOf(e)]=a:Xt.push(a)),a.patchFlag=-2,a}if(M0(e)&&(e=e.__vccOpts),t){t=O0(t);let{class:a,style:l}=t;a&&!Ae(a)&&(t.class=U(a)),Oe(l)&&(ja(l)&&!pe(l)&&(l=vt({},l)),t.style=ot(l))}const i=Ae(e)?1:Ov(e)?128:Xh(e)?64:Oe(e)?4:ve(e)?2:0;return le(e,t,n,r,o,i,s,!0)}function O0(e){return e?ja(e)||bv(e)?vt({},e):e:null}function fr(e,t,n=!1,r=!1){const{props:o,ref:s,patchFlag:i,children:a,transition:l}=e,u=t?En(o||{},t):o,c={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&Rv(u),ref:t&&t.ref?n&&s?pe(s)?s.concat(Yi(t)):[s,Yi(t)]:Yi(t):s,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:a,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==nt?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:l,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&fr(e.ssContent),ssFallback:e.ssFallback&&fr(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return l&&r&&uo(c,l.clone(c)),c}function Un(e=" ",t=0){return re(Zo,null,e,t)}function ae(e="",t=!1){return t?(L(),fe(_t,null,e)):re(_t,null,e)}function Vn(e){return e==null||typeof e=="boolean"?re(_t):pe(e)?re(nt,null,e.slice()):cn(e)?tr(e):re(Zo,null,String(e))}function tr(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:fr(e)}function fc(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(pe(t))n=16;else if(typeof t=="object")if(r&65){const o=t.default;o&&(o._c&&(o._d=!1),fc(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;!o&&!bv(t)?t._ctx=Ct:o===3&&Ct&&(Ct.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else ve(t)?(t={default:t,_ctx:Ct},n=32):(t=String(t),r&64?(n=16,t=[Un(t)]):n=8);e.children=t,e.shapeFlag|=n}function En(...e){const t={};for(let n=0;nPt||Ct;let ha,uu;{const e=ka(),t=(n,r)=>{let o;return(o=e[n])||(o=e[n]=[]),o.push(r),s=>{o.length>1?o.forEach(i=>i(s)):o[0](s)}};ha=t("__VUE_INSTANCE_SETTERS__",n=>Pt=n),uu=t("__VUE_SSR_SETTERS__",n=>Hs=n)}const li=e=>{const t=Pt;return ha(e),e.scope.on(),()=>{e.scope.off(),ha(t)}},Cf=()=>{Pt&&Pt.scope.off(),ha(null)};function xv(e){return e.vnode.shapeFlag&4}let Hs=!1;function I0(e,t=!1,n=!1){t&&uu(t);const{props:r,children:o}=e.vnode,s=xv(e);p0(e,r,s,t),g0(e,o,n||t);const i=s?P0(e,t):void 0;return t&&uu(!1),i}function P0(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Xy);const{setup:r}=n;if(r){lr();const o=e.setupContext=r.length>1?Pv(e):null,s=li(e),i=ai(r,e,0,[e.props,o]),a=ia(i);if(ur(),s(),(a||e.sp)&&!Fo(e)&&ov(e),a){if(i.then(Cf,Cf),t)return i.then(l=>{Tf(e,l)}).catch(l=>{za(l,e,0)});e.asyncDep=i}else Tf(e,i)}else Iv(e)}function Tf(e,t,n){ve(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Oe(t)&&(e.setupState=jh(t)),Iv(e)}function Iv(e,t,n){const r=e.type;e.render||(e.render=r.render||ht);{const o=li(e);lr();try{Qy(e)}finally{ur(),o()}}}const N0={get(e,t){return It(e,"get",""),e[t]}};function Pv(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,N0),slots:e.slots,emit:e.emit,expose:t}}function Ga(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(jh(Ds(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Ts)return Ts[n](e)},has(t,n){return n in t||n in Ts}})):e.proxy}function L0(e,t=!0){return ve(e)?e.displayName||e.name:e.name||t&&e.__name}function M0(e){return ve(e)&&"__vccOpts"in e}const C=(e,t)=>Py(e,t,Hs);function or(e,t,n){try{pa(-1);const r=arguments.length;return r===2?Oe(t)&&!pe(t)?cn(t)?re(e,null,[t]):re(e,t):re(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&cn(n)&&(n=[n]),re(e,t,n))}finally{pa(1)}}const $0="3.5.30",k0=ht;/** +* @vue/runtime-dom v3.5.30 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let cu;const Of=typeof window<"u"&&window.trustedTypes;if(Of)try{cu=Of.createPolicy("vue",{createHTML:e=>e})}catch{}const Nv=cu?e=>cu.createHTML(e):e=>e,F0="http://www.w3.org/2000/svg",B0="http://www.w3.org/1998/Math/MathML",Zn=typeof document<"u"?document:null,Af=Zn&&Zn.createElement("template"),D0={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t==="svg"?Zn.createElementNS(F0,e):t==="mathml"?Zn.createElementNS(B0,e):n?Zn.createElement(e,{is:n}):Zn.createElement(e);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Zn.createTextNode(e),createComment:e=>Zn.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Zn.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{Af.innerHTML=Nv(r==="svg"?`${e}`:r==="mathml"?`${e}`:e);const a=Af.content;if(r==="svg"||r==="mathml"){const l=a.firstChild;for(;l.firstChild;)a.appendChild(l.firstChild);a.removeChild(l)}t.insertBefore(a,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Sr="transition",cs="animation",Vo=Symbol("_vtc"),Lv={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Mv=vt({},ev,Lv),V0=e=>(e.displayName="Transition",e.props=Mv,e),Fr=V0((e,{slots:t})=>or(Hy,$v(e),t)),qr=(e,t=[])=>{pe(e)?e.forEach(n=>n(...t)):e&&e(...t)},Rf=e=>e?pe(e)?e.some(t=>t.length>1):e.length>1:!1;function $v(e){const t={};for(const $ in e)$ in Lv||(t[$]=e[$]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:a=`${n}-enter-to`,appearFromClass:l=s,appearActiveClass:u=i,appearToClass:c=a,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:v=`${n}-leave-to`}=e,p=j0(o),h=p&&p[0],b=p&&p[1],{onBeforeEnter:m,onEnter:S,onEnterCancelled:_,onLeave:w,onLeaveCancelled:y,onBeforeAppear:A=m,onAppear:R=S,onAppearCancelled:N=_}=t,I=($,F,Y,P)=>{$._enterCancelled=P,Cr($,F?c:a),Cr($,F?u:i),Y&&Y()},x=($,F)=>{$._isLeaving=!1,Cr($,f),Cr($,v),Cr($,d),F&&F()},k=$=>(F,Y)=>{const P=$?R:S,O=()=>I(F,$,Y);qr(P,[F,O]),xf(()=>{Cr(F,$?l:s),$n(F,$?c:a),Rf(P)||If(F,r,h,O)})};return vt(t,{onBeforeEnter($){qr(m,[$]),$n($,s),$n($,i)},onBeforeAppear($){qr(A,[$]),$n($,l),$n($,u)},onEnter:k(!1),onAppear:k(!0),onLeave($,F){$._isLeaving=!0;const Y=()=>x($,F);$n($,f),$._enterCancelled?($n($,d),fu($)):(fu($),$n($,d)),xf(()=>{$._isLeaving&&(Cr($,f),$n($,v),Rf(w)||If($,r,b,Y))}),qr(w,[$,Y])},onEnterCancelled($){I($,!1,void 0,!0),qr(_,[$])},onAppearCancelled($){I($,!0,void 0,!0),qr(N,[$])},onLeaveCancelled($){x($),qr(y,[$])}})}function j0(e){if(e==null)return null;if(Oe(e))return[Ol(e.enter),Ol(e.leave)];{const t=Ol(e);return[t,t]}}function Ol(e){return Jb(e)}function $n(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Vo]||(e[Vo]=new Set)).add(t)}function Cr(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[Vo];n&&(n.delete(t),n.size||(e[Vo]=void 0))}function xf(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let z0=0;function If(e,t,n,r){const o=e._endId=++z0,s=()=>{o===e._endId&&r()};if(n!=null)return setTimeout(s,n);const{type:i,timeout:a,propCount:l}=kv(e,t);if(!i)return r();const u=i+"end";let c=0;const f=()=>{e.removeEventListener(u,d),s()},d=v=>{v.target===e&&++c>=l&&f()};setTimeout(()=>{c(n[p]||"").split(", "),o=r(`${Sr}Delay`),s=r(`${Sr}Duration`),i=Pf(o,s),a=r(`${cs}Delay`),l=r(`${cs}Duration`),u=Pf(a,l);let c=null,f=0,d=0;t===Sr?i>0&&(c=Sr,f=i,d=s.length):t===cs?u>0&&(c=cs,f=u,d=l.length):(f=Math.max(i,u),c=f>0?i>u?Sr:cs:null,d=c?c===Sr?s.length:l.length:0);const v=c===Sr&&/\b(?:transform|all)(?:,|$)/.test(r(`${Sr}Property`).toString());return{type:c,timeout:f,propCount:d,hasTransform:v}}function Pf(e,t){for(;e.lengthNf(n)+Nf(e[r])))}function Nf(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function fu(e){return(e?e.ownerDocument:document).body.offsetHeight}function H0(e,t,n){const r=e[Vo];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const va=Symbol("_vod"),Fv=Symbol("_vsh"),en={name:"show",beforeMount(e,{value:t},{transition:n}){e[va]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):fs(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),fs(e,!0),r.enter(e)):r.leave(e,()=>{fs(e,!1)}):fs(e,t))},beforeUnmount(e,{value:t}){fs(e,t)}};function fs(e,t){e.style.display=t?e[va]:"none",e[Fv]=!t}const Bv=Symbol("");function bN(e){const t=Ge();if(!t)return;const n=t.ut=(o=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(s=>ma(s,o))},r=()=>{const o=e(t.proxy);t.ce?ma(t.ce,o):du(t.subTree,o),n(o)};iv(()=>{Kh(r)}),Ke(()=>{he(r,ht,{flush:"post"});const o=new MutationObserver(r);o.observe(t.subTree.el.parentNode,{childList:!0}),kr(()=>o.disconnect())})}function du(e,t){if(e.shapeFlag&128){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push(()=>{du(n.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)ma(e.el,t);else if(e.type===nt)e.children.forEach(n=>du(n,t));else if(e.type===Gi){let{el:n,anchor:r}=e;for(;n&&(ma(n,t),n!==r);)n=n.nextSibling}}function ma(e,t){if(e.nodeType===1){const n=e.style;let r="";for(const o in t){const s=oy(t[o]);n.setProperty(`--${o}`,s),r+=`--${o}: ${s};`}n[Bv]=r}}const U0=/(?:^|;)\s*display\s*:/;function K0(e,t,n){const r=e.style,o=Ae(n);let s=!1;if(n&&!o){if(t)if(Ae(t))for(const i of t.split(";")){const a=i.slice(0,i.indexOf(":")).trim();n[a]==null&&Ji(r,a,"")}else for(const i in t)n[i]==null&&Ji(r,i,"");for(const i in n)i==="display"&&(s=!0),Ji(r,i,n[i])}else if(o){if(t!==n){const i=r[Bv];i&&(n+=";"+i),r.cssText=n,s=U0.test(n)}}else t&&e.removeAttribute("style");va in e&&(e[va]=s?r.display:"",e[Fv]&&(r.display="none"))}const Lf=/\s*!important$/;function Ji(e,t,n){if(pe(n))n.forEach(r=>Ji(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=q0(e,t);Lf.test(n)?e.setProperty(hr(r),n.replace(Lf,""),"important"):e[r]=n}}const Mf=["Webkit","Moz","ms"],Al={};function q0(e,t){const n=Al[t];if(n)return n;let r=Mt(t);if(r!=="filter"&&r in e)return Al[t]=r;r=si(r);for(let o=0;oRl||(J0.then(()=>Rl=0),Rl=Date.now());function Z0(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;An(Q0(r,n.value),t,5,[r])};return n.value=e,n.attached=X0(),n}function Q0(e,t){if(pe(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Vf=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ew=(e,t,n,r,o,s)=>{const i=o==="svg";t==="class"?H0(e,r,i):t==="style"?K0(e,n,r):Na(t)?qu(t)||G0(e,t,n,r,s):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):tw(e,t,r,i))?(Ff(e,t,r),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&kf(e,t,r,i,s,t!=="value")):e._isVueCE&&(nw(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!Ae(r)))?Ff(e,Mt(t),r,s,t):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),kf(e,t,r,i))};function tw(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&Vf(t)&&ve(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const o=e.tagName;if(o==="IMG"||o==="VIDEO"||o==="CANVAS"||o==="SOURCE")return!1}return Vf(t)&&Ae(n)?!1:t in e}function nw(e,t){const n=e._def.props;if(!n)return!1;const r=Mt(t);return Array.isArray(n)?n.some(o=>Mt(o)===r):Object.keys(n).some(o=>Mt(o)===r)}const Dv=new WeakMap,Vv=new WeakMap,ga=Symbol("_moveCb"),jf=Symbol("_enterCb"),rw=e=>(delete e.props.mode,e),ow=rw({name:"TransitionGroup",props:vt({},Mv,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=Ge(),r=Qh();let o,s;return mo(()=>{if(!o.length)return;const i=e.moveClass||`${e.name||"v"}-move`;if(!uw(o[0].el,n.vnode.el,i)){o=[];return}o.forEach(iw),o.forEach(aw);const a=o.filter(lw);fu(n.vnode.el),a.forEach(l=>{const u=l.el,c=u.style;$n(u,i),c.transform=c.webkitTransform=c.transitionDuration="";const f=u[ga]=d=>{d&&d.target!==u||(!d||d.propertyName.endsWith("transform"))&&(u.removeEventListener("transitionend",f),u[ga]=null,Cr(u,i))};u.addEventListener("transitionend",f)}),o=[]}),()=>{const i=Me(e),a=$v(i);let l=i.tag||nt;if(o=[],s)for(let u=0;u{a.split(/\s+/).forEach(l=>l&&r.classList.remove(l))}),n.split(/\s+/).forEach(a=>a&&r.classList.add(a)),r.style.display="none";const s=t.nodeType===1?t:t.parentNode;s.appendChild(r);const{hasTransform:i}=kv(r);return s.removeChild(r),i}const ba=e=>{const t=e.props["onUpdate:modelValue"]||!1;return pe(t)?n=>qi(t,n):t};function cw(e){e.target.composing=!0}function zf(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Bo=Symbol("_assign");function Hf(e,t,n){return t&&(e=e.trim()),n&&(e=Gu(e)),e}const fw={created(e,{modifiers:{lazy:t,trim:n,number:r}},o){e[Bo]=ba(o);const s=r||o.props&&o.props.type==="number";Zr(e,t?"change":"input",i=>{i.target.composing||e[Bo](Hf(e.value,n,s))}),(n||s)&&Zr(e,"change",()=>{e.value=Hf(e.value,n,s)}),t||(Zr(e,"compositionstart",cw),Zr(e,"compositionend",zf),Zr(e,"change",zf))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:o,number:s}},i){if(e[Bo]=ba(i),e.composing)return;const a=(s||e.type==="number")&&!/^0\d/.test(e.value)?Gu(e.value):e.value,l=t??"";a!==l&&(document.activeElement===e&&e.type!=="range"&&(r&&t===n||o&&e.value.trim()===l)||(e.value=l))}},ya={deep:!0,created(e,t,n){e[Bo]=ba(n),Zr(e,"change",()=>{const r=e._modelValue,o=dw(e),s=e.checked,i=e[Bo];if(pe(r)){const a=Sh(r,o),l=a!==-1;if(s&&!l)i(r.concat(o));else if(!s&&l){const u=[...r];u.splice(a,1),i(u)}}else if(La(r)){const a=new Set(r);s?a.add(o):a.delete(o),i(a)}else i(zv(e,s))})},mounted:Uf,beforeUpdate(e,t,n){e[Bo]=ba(n),Uf(e,t,n)}};function Uf(e,{value:t,oldValue:n},r){e._modelValue=t;let o;if(pe(t))o=Sh(t,r.props.value)>-1;else if(La(t))o=t.has(r.props.value);else{if(t===n)return;o=ii(t,zv(e,!0))}e.checked!==o&&(e.checked=o)}function dw(e){return"_value"in e?e._value:e.value}function zv(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const pw=["ctrl","shift","alt","meta"],hw={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>pw.some(n=>e[`${n}Key`]&&!t.includes(n))},tt=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=(o,...s)=>{for(let i=0;i{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=o=>{if(!("key"in o))return;const s=hr(o.key);if(t.some(i=>i===s||vw[i]===s))return e(o)})},mw=vt({patchProp:ew},D0);let Kf;function Hv(){return Kf||(Kf=y0(mw))}const wa=(...e)=>{Hv().render(...e)},gw=(...e)=>{const t=Hv().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=yw(r);if(!o)return;const s=t._component;!ve(s)&&!s.render&&!s.template&&(s.template=o.innerHTML),o.nodeType===1&&(o.textContent="");const i=n(o,!1,bw(o));return o instanceof Element&&(o.removeAttribute("v-cloak"),o.setAttribute("data-v-app","")),i},t};function bw(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function yw(e){return Ae(e)?document.querySelector(e):e}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Io=typeof document<"u";function Uv(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function ww(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Uv(e.default)}const We=Object.assign;function xl(e,t){const n={};for(const r in t){const o=t[r];n[r]=Rn(o)?o.map(e):e(o)}return n}const As=()=>{},Rn=Array.isArray;function qf(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}const Kv=/#/g,Sw=/&/g,Ew=/\//g,_w=/=/g,Cw=/\?/g,qv=/\+/g,Tw=/%5B/g,Ow=/%5D/g,Wv=/%5E/g,Aw=/%60/g,Gv=/%7B/g,Rw=/%7C/g,Yv=/%7D/g,xw=/%20/g;function dc(e){return e==null?"":encodeURI(""+e).replace(Rw,"|").replace(Tw,"[").replace(Ow,"]")}function Iw(e){return dc(e).replace(Gv,"{").replace(Yv,"}").replace(Wv,"^")}function pu(e){return dc(e).replace(qv,"%2B").replace(xw,"+").replace(Kv,"%23").replace(Sw,"%26").replace(Aw,"`").replace(Gv,"{").replace(Yv,"}").replace(Wv,"^")}function Pw(e){return pu(e).replace(_w,"%3D")}function Nw(e){return dc(e).replace(Kv,"%23").replace(Cw,"%3F")}function Lw(e){return Nw(e).replace(Ew,"%2F")}function Us(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const Mw=/\/$/,$w=e=>e.replace(Mw,"");function Il(e,t,n="/"){let r,o={},s="",i="";const a=t.indexOf("#");let l=t.indexOf("?");return l=a>=0&&l>a?-1:l,l>=0&&(r=t.slice(0,l),s=t.slice(l,a>0?a:t.length),o=e(s.slice(1))),a>=0&&(r=r||t.slice(0,a),i=t.slice(a,t.length)),r=Dw(r??t,n),{fullPath:r+s+i,path:r,query:o,hash:Us(i)}}function kw(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Wf(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Fw(e,t,n){const r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&jo(t.matched[r],n.matched[o])&&Jv(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function jo(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Jv(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Bw(e[n],t[n]))return!1;return!0}function Bw(e,t){return Rn(e)?Gf(e,t):Rn(t)?Gf(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function Gf(e,t){return Rn(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function Dw(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];(o===".."||o===".")&&r.push("");let s=n.length-1,i,a;for(i=0;i1&&s--;else break;return n.slice(0,s).join("/")+"/"+r.slice(i).join("/")}const Er={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let hu=function(e){return e.pop="pop",e.push="push",e}({}),Pl=function(e){return e.back="back",e.forward="forward",e.unknown="",e}({});function Vw(e){if(!e)if(Io){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),$w(e)}const jw=/^[^#]+#/;function zw(e,t){return e.replace(jw,"#")+t}function Hw(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Ya=()=>({left:window.scrollX,top:window.scrollY});function Uw(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.startsWith("#"),o=typeof n=="string"?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=Hw(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Yf(e,t){return(history.state?history.state.position-t:-1)+e}const vu=new Map;function Kw(e,t){vu.set(e,t)}function qw(e){const t=vu.get(e);return vu.delete(e),t}function Ww(e){return typeof e=="string"||e&&typeof e=="object"}function Xv(e){return typeof e=="string"||typeof e=="symbol"}let ut=function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e}({});const Zv=Symbol("");ut.MATCHER_NOT_FOUND+"",ut.NAVIGATION_GUARD_REDIRECT+"",ut.NAVIGATION_ABORTED+"",ut.NAVIGATION_CANCELLED+"",ut.NAVIGATION_DUPLICATED+"";function zo(e,t){return We(new Error,{type:e,[Zv]:!0},t)}function Jn(e,t){return e instanceof Error&&Zv in e&&(t==null||!!(e.type&t))}const Gw=["params","query","hash"];function Yw(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of Gw)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function Jw(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;ro&&pu(o)):[r&&pu(r)]).forEach(o=>{o!==void 0&&(t+=(t.length?"&":"")+n,o!=null&&(t+="="+o))})}return t}function Xw(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[n]=Rn(r)?r.map(o=>o==null?null:""+o):r==null?r:""+r)}return t}const Qv=Symbol(""),Xf=Symbol(""),Ja=Symbol(""),em=Symbol(""),mu=Symbol("");function ds(){let e=[];function t(r){return e.push(r),()=>{const o=e.indexOf(r);o>-1&&e.splice(o,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function Zw(e,t,n){const r=()=>{e[t].delete(n)};kr(r),oc(r),Ka(()=>{e[t].add(n)}),e[t].add(n)}function yN(e){const t=Ee(Qv,{}).value;t&&Zw(t,"leaveGuards",e)}function Ir(e,t,n,r,o,s=i=>i()){const i=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise((a,l)=>{const u=d=>{d===!1?l(zo(ut.NAVIGATION_ABORTED,{from:n,to:t})):d instanceof Error?l(d):Ww(d)?l(zo(ut.NAVIGATION_GUARD_REDIRECT,{from:t,to:d})):(i&&r.enterCallbacks[o]===i&&typeof d=="function"&&i.push(d),a())},c=s(()=>e.call(r&&r.instances[o],t,n,u));let f=Promise.resolve(c);e.length<3&&(f=f.then(u)),f.catch(d=>l(d))})}function Nl(e,t,n,r,o=s=>s()){const s=[];for(const i of e)for(const a in i.components){let l=i.components[a];if(!(t!=="beforeRouteEnter"&&!i.instances[a]))if(Uv(l)){const u=(l.__vccOpts||l)[t];u&&s.push(Ir(u,n,r,i,a,o))}else{let u=l();s.push(()=>u.then(c=>{if(!c)throw new Error(`Couldn't resolve component "${a}" at "${i.path}"`);const f=ww(c)?c.default:c;i.mods[a]=c,i.components[a]=f;const d=(f.__vccOpts||f)[t];return d&&Ir(d,n,r,i,a,o)()}))}}return s}function Qw(e,t){const n=[],r=[],o=[],s=Math.max(t.matched.length,e.matched.length);for(let i=0;ijo(u,a))?r.push(a):n.push(a));const l=e.matched[i];l&&(t.matched.find(u=>jo(u,l))||o.push(l))}return[n,r,o]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let e1=()=>location.protocol+"//"+location.host;function tm(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let i=o.includes(e.slice(s))?e.slice(s).length:1,a=o.slice(i);return a[0]!=="/"&&(a="/"+a),Wf(a,"")}return Wf(n,e)+r+o}function t1(e,t,n,r){let o=[],s=[],i=null;const a=({state:d})=>{const v=tm(e,location),p=n.value,h=t.value;let b=0;if(d){if(n.value=v,t.value=d,i&&i===p){i=null;return}b=h?d.position-h.position:0}else r(v);o.forEach(m=>{m(n.value,p,{delta:b,type:hu.pop,direction:b?b>0?Pl.forward:Pl.back:Pl.unknown})})};function l(){i=n.value}function u(d){o.push(d);const v=()=>{const p=o.indexOf(d);p>-1&&o.splice(p,1)};return s.push(v),v}function c(){if(document.visibilityState==="hidden"){const{history:d}=window;if(!d.state)return;d.replaceState(We({},d.state,{scroll:Ya()}),"")}}function f(){for(const d of s)d();s=[],window.removeEventListener("popstate",a),window.removeEventListener("pagehide",c),document.removeEventListener("visibilitychange",c)}return window.addEventListener("popstate",a),window.addEventListener("pagehide",c),document.addEventListener("visibilitychange",c),{pauseListeners:l,listen:u,destroy:f}}function Zf(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Ya():null}}function n1(e){const{history:t,location:n}=window,r={value:tm(e,n)},o={value:t.state};o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function s(l,u,c){const f=e.indexOf("#"),d=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+l:e1()+e+l;try{t[c?"replaceState":"pushState"](u,"",d),o.value=u}catch{n[c?"replace":"assign"](d)}}function i(l,u){s(l,We({},t.state,Zf(o.value.back,l,o.value.forward,!0),u,{position:o.value.position}),!0),r.value=l}function a(l,u){const c=We({},o.value,t.state,{forward:l,scroll:Ya()});s(c.current,c,!0),s(l,We({},Zf(r.value,l,null),{position:c.position+1},u),!1),r.value=l}return{location:r,state:o,push:a,replace:i}}function r1(e){e=Vw(e);const t=n1(e),n=t1(e,t.state,t.location,t.replace);function r(s,i=!0){i||n.pauseListeners(),history.go(s)}const o=We({location:"",base:e,go:r,createHref:zw.bind(null,e)},t,n);return Object.defineProperty(o,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(o,"state",{enumerable:!0,get:()=>t.state.value}),o}function wN(e){return e=location.host?e||location.pathname+location.search:"",e.includes("#")||(e+="#"),r1(e)}let Qr=function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e}({});var gt=function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e}(gt||{});const o1={type:Qr.Static,value:""},s1=/[a-zA-Z0-9_]/;function i1(e){if(!e)return[[]];if(e==="/")return[[o1]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(v){throw new Error(`ERR (${n})/"${u}": ${v}`)}let n=gt.Static,r=n;const o=[];let s;function i(){s&&o.push(s),s=[]}let a=0,l,u="",c="";function f(){u&&(n===gt.Static?s.push({type:Qr.Static,value:u}):n===gt.Param||n===gt.ParamRegExp||n===gt.ParamRegExpEnd?(s.length>1&&(l==="*"||l==="+")&&t(`A repeatable param (${u}) must be alone in its segment. eg: '/:ids+.`),s.push({type:Qr.Param,value:u,regexp:c,repeatable:l==="*"||l==="+",optional:l==="*"||l==="?"})):t("Invalid state to consume buffer"),u="")}function d(){u+=l}for(;at.length?t.length===1&&t[0]===kt.Static+kt.Segment?1:-1:0}function nm(e,t){let n=0;const r=e.score,o=t.score;for(;n0&&t[t.length-1]<0}const f1={strict:!1,end:!0,sensitive:!1};function d1(e,t,n){const r=u1(i1(e.path),n),o=We(r,{record:e,parent:t,children:[],alias:[]});return t&&!o.record.aliasOf==!t.record.aliasOf&&t.children.push(o),o}function p1(e,t){const n=[],r=new Map;t=qf(f1,t);function o(f){return r.get(f)}function s(f,d,v){const p=!v,h=nd(f);h.aliasOf=v&&v.record;const b=qf(t,f),m=[h];if("alias"in f){const w=typeof f.alias=="string"?[f.alias]:f.alias;for(const y of w)m.push(nd(We({},h,{components:v?v.record.components:h.components,path:y,aliasOf:v?v.record:h})))}let S,_;for(const w of m){const{path:y}=w;if(d&&y[0]!=="/"){const A=d.record.path,R=A[A.length-1]==="/"?"":"/";w.path=d.record.path+(y&&R+y)}if(S=d1(w,d,b),v?v.alias.push(S):(_=_||S,_!==S&&_.alias.push(S),p&&f.name&&!rd(S)&&i(f.name)),rm(S)&&l(S),h.children){const A=h.children;for(let R=0;R{i(_)}:As}function i(f){if(Xv(f)){const d=r.get(f);d&&(r.delete(f),n.splice(n.indexOf(d),1),d.children.forEach(i),d.alias.forEach(i))}else{const d=n.indexOf(f);d>-1&&(n.splice(d,1),f.record.name&&r.delete(f.record.name),f.children.forEach(i),f.alias.forEach(i))}}function a(){return n}function l(f){const d=m1(f,n);n.splice(d,0,f),f.record.name&&!rd(f)&&r.set(f.record.name,f)}function u(f,d){let v,p={},h,b;if("name"in f&&f.name){if(v=r.get(f.name),!v)throw zo(ut.MATCHER_NOT_FOUND,{location:f});b=v.record.name,p=We(td(d.params,v.keys.filter(_=>!_.optional).concat(v.parent?v.parent.keys.filter(_=>_.optional):[]).map(_=>_.name)),f.params&&td(f.params,v.keys.map(_=>_.name))),h=v.stringify(p)}else if(f.path!=null)h=f.path,v=n.find(_=>_.re.test(h)),v&&(p=v.parse(h),b=v.record.name);else{if(v=d.name?r.get(d.name):n.find(_=>_.re.test(d.path)),!v)throw zo(ut.MATCHER_NOT_FOUND,{location:f,currentLocation:d});b=v.record.name,p=We({},d.params,f.params),h=v.stringify(p)}const m=[];let S=v;for(;S;)m.unshift(S.record),S=S.parent;return{name:b,path:h,params:p,matched:m,meta:v1(m)}}e.forEach(f=>s(f));function c(){n.length=0,r.clear()}return{addRoute:s,resolve:u,removeRoute:i,clearRoutes:c,getRoutes:a,getRecordMatcher:o}}function td(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function nd(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:h1(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function h1(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]=typeof n=="object"?n[r]:n;return t}function rd(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function v1(e){return e.reduce((t,n)=>We(t,n.meta),{})}function m1(e,t){let n=0,r=t.length;for(;n!==r;){const s=n+r>>1;nm(e,t[s])<0?r=s:n=s+1}const o=g1(e);return o&&(r=t.lastIndexOf(o,r-1)),r}function g1(e){let t=e;for(;t=t.parent;)if(rm(t)&&nm(e,t)===0)return t}function rm({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function od(e){const t=Ee(Ja),n=Ee(em),r=C(()=>{const l=g(e.to);return t.resolve(l)}),o=C(()=>{const{matched:l}=r.value,{length:u}=l,c=l[u-1],f=n.matched;if(!c||!f.length)return-1;const d=f.findIndex(jo.bind(null,c));if(d>-1)return d;const v=sd(l[u-2]);return u>1&&sd(c)===v&&f[f.length-1].path!==v?f.findIndex(jo.bind(null,l[u-2])):d}),s=C(()=>o.value>-1&&E1(n.params,r.value.params)),i=C(()=>o.value>-1&&o.value===n.matched.length-1&&Jv(n.params,r.value.params));function a(l={}){if(S1(l)){const u=t[g(e.replace)?"replace":"push"](g(e.to)).catch(As);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>u),u}return Promise.resolve()}return{route:r,href:C(()=>r.value.href),isActive:s,isExactActive:i,navigate:a}}function b1(e){return e.length===1?e[0]:e}const y1=Z({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:od,setup(e,{slots:t}){const n=wt(od(e)),{options:r}=Ee(Ja),o=C(()=>({[id(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[id(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const s=t.default&&b1(t.default(n));return e.custom?s:or("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:o.value},s)}}}),w1=y1;function S1(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function E1(e,t){for(const n in t){const r=t[n],o=e[n];if(typeof r=="string"){if(r!==o)return!1}else if(!Rn(o)||o.length!==r.length||r.some((s,i)=>s.valueOf()!==o[i].valueOf()))return!1}return!0}function sd(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const id=(e,t,n)=>e??t??n,_1=Z({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=Ee(mu),o=C(()=>e.route||r.value),s=Ee(Xf,0),i=C(()=>{let u=g(s);const{matched:c}=o.value;let f;for(;(f=c[u])&&!f.components;)u++;return u}),a=C(()=>o.value.matched[i.value]);ft(Xf,C(()=>i.value+1)),ft(Qv,a),ft(mu,o);const l=V();return he(()=>[l.value,a.value,e.name],([u,c,f],[d,v,p])=>{c&&(c.instances[f]=u,v&&v!==c&&u&&u===d&&(c.leaveGuards.size||(c.leaveGuards=v.leaveGuards),c.updateGuards.size||(c.updateGuards=v.updateGuards))),u&&c&&(!v||!jo(c,v)||!d)&&(c.enterCallbacks[f]||[]).forEach(h=>h(u))},{flush:"post"}),()=>{const u=o.value,c=e.name,f=a.value,d=f&&f.components[c];if(!d)return ad(n.default,{Component:d,route:u});const v=f.props[c],p=v?v===!0?u.params:typeof v=="function"?v(u):v:null,b=or(d,We({},p,t,{onVnodeUnmounted:m=>{m.component.isUnmounted&&(f.instances[c]=null)},ref:l}));return ad(n.default,{Component:b,route:u})||b}}});function ad(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const C1=_1;function SN(e){const t=p1(e.routes,e),n=e.parseQuery||Jw,r=e.stringifyQuery||Jf,o=e.history,s=ds(),i=ds(),a=ds(),l=ir(Er);let u=Er;Io&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const c=xl.bind(null,B=>""+B),f=xl.bind(null,Lw),d=xl.bind(null,Us);function v(B,K){let J,oe;return Xv(B)?(J=t.getRecordMatcher(B),oe=K):oe=B,t.addRoute(oe,J)}function p(B){const K=t.getRecordMatcher(B);K&&t.removeRoute(K)}function h(){return t.getRoutes().map(B=>B.record)}function b(B){return!!t.getRecordMatcher(B)}function m(B,K){if(K=We({},K||l.value),typeof B=="string"){const M=Il(n,B,K.path),W=t.resolve({path:M.path},K),G=o.createHref(M.fullPath);return We(M,W,{params:d(W.params),hash:Us(M.hash),redirectedFrom:void 0,href:G})}let J;if(B.path!=null)J=We({},B,{path:Il(n,B.path,K.path).path});else{const M=We({},B.params);for(const W in M)M[W]==null&&delete M[W];J=We({},B,{params:f(M)}),K.params=f(K.params)}const oe=t.resolve(J,K),ge=B.hash||"";oe.params=c(d(oe.params));const E=kw(r,We({},B,{hash:Iw(ge),path:oe.path})),T=o.createHref(E);return We({fullPath:E,hash:ge,query:r===Jf?Xw(B.query):B.query||{}},oe,{redirectedFrom:void 0,href:T})}function S(B){return typeof B=="string"?Il(n,B,l.value.path):We({},B)}function _(B,K){if(u!==B)return zo(ut.NAVIGATION_CANCELLED,{from:K,to:B})}function w(B){return R(B)}function y(B){return w(We(S(B),{replace:!0}))}function A(B,K){const J=B.matched[B.matched.length-1];if(J&&J.redirect){const{redirect:oe}=J;let ge=typeof oe=="function"?oe(B,K):oe;return typeof ge=="string"&&(ge=ge.includes("?")||ge.includes("#")?ge=S(ge):{path:ge},ge.params={}),We({query:B.query,hash:B.hash,params:ge.path!=null?{}:B.params},ge)}}function R(B,K){const J=u=m(B),oe=l.value,ge=B.state,E=B.force,T=B.replace===!0,M=A(J,oe);if(M)return R(We(S(M),{state:typeof M=="object"?We({},ge,M.state):ge,force:E,replace:T}),K||J);const W=J;W.redirectedFrom=K;let G;return!E&&Fw(r,oe,J)&&(G=zo(ut.NAVIGATION_DUPLICATED,{to:W,from:oe}),Re(oe,oe,!0,!1)),(G?Promise.resolve(G):x(W,oe)).catch(q=>Jn(q)?Jn(q,ut.NAVIGATION_GUARD_REDIRECT)?q:Pe(q):Q(q,W,oe)).then(q=>{if(q){if(Jn(q,ut.NAVIGATION_GUARD_REDIRECT))return R(We({replace:T},S(q.to),{state:typeof q.to=="object"?We({},ge,q.to.state):ge,force:E}),K||W)}else q=$(W,oe,!0,T,ge);return k(W,oe,q),q})}function N(B,K){const J=_(B,K);return J?Promise.reject(J):Promise.resolve()}function I(B){const K=qe.values().next().value;return K&&typeof K.runWithContext=="function"?K.runWithContext(B):B()}function x(B,K){let J;const[oe,ge,E]=Qw(B,K);J=Nl(oe.reverse(),"beforeRouteLeave",B,K);for(const M of oe)M.leaveGuards.forEach(W=>{J.push(Ir(W,B,K))});const T=N.bind(null,B,K);return J.push(T),De(J).then(()=>{J=[];for(const M of s.list())J.push(Ir(M,B,K));return J.push(T),De(J)}).then(()=>{J=Nl(ge,"beforeRouteUpdate",B,K);for(const M of ge)M.updateGuards.forEach(W=>{J.push(Ir(W,B,K))});return J.push(T),De(J)}).then(()=>{J=[];for(const M of E)if(M.beforeEnter)if(Rn(M.beforeEnter))for(const W of M.beforeEnter)J.push(Ir(W,B,K));else J.push(Ir(M.beforeEnter,B,K));return J.push(T),De(J)}).then(()=>(B.matched.forEach(M=>M.enterCallbacks={}),J=Nl(E,"beforeRouteEnter",B,K,I),J.push(T),De(J))).then(()=>{J=[];for(const M of i.list())J.push(Ir(M,B,K));return J.push(T),De(J)}).catch(M=>Jn(M,ut.NAVIGATION_CANCELLED)?M:Promise.reject(M))}function k(B,K,J){a.list().forEach(oe=>I(()=>oe(B,K,J)))}function $(B,K,J,oe,ge){const E=_(B,K);if(E)return E;const T=K===Er,M=Io?history.state:{};J&&(oe||T?o.replace(B.fullPath,We({scroll:T&&M&&M.scroll},ge)):o.push(B.fullPath,ge)),l.value=B,Re(B,K,J,T),Pe()}let F;function Y(){F||(F=o.listen((B,K,J)=>{if(!ze.listening)return;const oe=m(B),ge=A(oe,ze.currentRoute.value);if(ge){R(We(ge,{replace:!0,force:!0}),oe).catch(As);return}u=oe;const E=l.value;Io&&Kw(Yf(E.fullPath,J.delta),Ya()),x(oe,E).catch(T=>Jn(T,ut.NAVIGATION_ABORTED|ut.NAVIGATION_CANCELLED)?T:Jn(T,ut.NAVIGATION_GUARD_REDIRECT)?(R(We(S(T.to),{force:!0}),oe).then(M=>{Jn(M,ut.NAVIGATION_ABORTED|ut.NAVIGATION_DUPLICATED)&&!J.delta&&J.type===hu.pop&&o.go(-1,!1)}).catch(As),Promise.reject()):(J.delta&&o.go(-J.delta,!1),Q(T,oe,E))).then(T=>{T=T||$(oe,E,!1),T&&(J.delta&&!Jn(T,ut.NAVIGATION_CANCELLED)?o.go(-J.delta,!1):J.type===hu.pop&&Jn(T,ut.NAVIGATION_ABORTED|ut.NAVIGATION_DUPLICATED)&&o.go(-1,!1)),k(oe,E,T)}).catch(As)}))}let P=ds(),O=ds(),j;function Q(B,K,J){Pe(B);const oe=O.list();return oe.length&&oe.forEach(ge=>ge(B,K,J)),Promise.reject(B)}function me(){return j&&l.value!==Er?Promise.resolve():new Promise((B,K)=>{P.add([B,K])})}function Pe(B){return j||(j=!B,Y(),P.list().forEach(([K,J])=>B?J(B):K()),P.reset()),B}function Re(B,K,J,oe){const{scrollBehavior:ge}=e;if(!Io||!ge)return Promise.resolve();const E=!J&&qw(Yf(B.fullPath,0))||(oe||!J)&&history.state&&history.state.scroll||null;return Ie().then(()=>ge(B,K,E)).then(T=>T&&Uw(T)).catch(T=>Q(T,B,K))}const Ce=B=>o.go(B);let _e;const qe=new Set,ze={currentRoute:l,listening:!0,addRoute:v,removeRoute:p,clearRoutes:t.clearRoutes,hasRoute:b,getRoutes:h,resolve:m,options:e,push:w,replace:y,go:Ce,back:()=>Ce(-1),forward:()=>Ce(1),beforeEach:s.add,beforeResolve:i.add,afterEach:a.add,onError:O.add,isReady:me,install(B){B.component("RouterLink",w1),B.component("RouterView",C1),B.config.globalProperties.$router=ze,Object.defineProperty(B.config.globalProperties,"$route",{enumerable:!0,get:()=>g(l)}),Io&&!_e&&l.value===Er&&(_e=!0,w(o.location).catch(oe=>{}));const K={};for(const oe in Er)Object.defineProperty(K,oe,{get:()=>l.value[oe],enumerable:!0});B.provide(Ja,ze),B.provide(em,Qu(K)),B.provide(mu,l);const J=B.unmount;qe.add(B),B.unmount=function(){qe.delete(B),qe.size<1&&(u=Er,F&&F(),F=null,l.value=Er,_e=!1,j=!1),J()}}};function De(B){return B.reduce((K,J)=>K.then(()=>I(J)),Promise.resolve())}return ze}function EN(){return Ee(Ja)}const T1='a[href],button:not([disabled]),button:not([hidden]),:not([tabindex="-1"]),input:not([disabled]),input:not([type="hidden"]),select:not([disabled]),textarea:not([disabled])',O1=e=>getComputedStyle(e).position==="fixed"?!1:e.offsetParent!==null,ld=e=>Array.from(e.querySelectorAll(T1)).filter(t=>A1(t)&&O1(t)),A1=e=>{if(e.tabIndex>0||e.tabIndex===0&&e.getAttribute("tabIndex")!==null)return!0;if(e.disabled)return!1;switch(e.nodeName){case"A":return!!e.href&&e.rel!=="ignore";case"INPUT":return!(e.type==="hidden"||e.type==="file");case"BUTTON":case"SELECT":case"TEXTAREA":return!0;default:return!1}},Qn=(e,t,{checkForDefaultPrevented:n=!0}={})=>o=>{const s=e==null?void 0:e(o);if(n===!1||!s)return t==null?void 0:t(o)};var R1=Object.defineProperty,x1=Object.defineProperties,I1=Object.getOwnPropertyDescriptors,ud=Object.getOwnPropertySymbols,P1=Object.prototype.hasOwnProperty,N1=Object.prototype.propertyIsEnumerable,cd=(e,t,n)=>t in e?R1(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,L1=(e,t)=>{for(var n in t||(t={}))P1.call(t,n)&&cd(e,n,t[n]);if(ud)for(var n of ud(t))N1.call(t,n)&&cd(e,n,t[n]);return e},M1=(e,t)=>x1(e,I1(t));function fd(e,t){var n;const r=ir();return Ha(()=>{r.value=e()},M1(L1({},t),{flush:(n=void 0)!=null?n:"sync"})),Mr(r)}var dd;const st=typeof window<"u",$1=e=>typeof e=="string",Sa=()=>{},gu=st&&((dd=window==null?void 0:window.navigator)==null?void 0:dd.userAgent)&&/iP(ad|hone|od)/.test(window.navigator.userAgent);function Ks(e){return typeof e=="function"?e():g(e)}function k1(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}function F1(e,t={}){let n,r,o=Sa;const s=a=>{clearTimeout(a),o(),o=Sa};return a=>{const l=Ks(e),u=Ks(t.maxWait);return n&&s(n),l<=0||u!==void 0&&u<=0?(r&&(s(r),r=null),Promise.resolve(a())):new Promise((c,f)=>{o=t.rejectOnCancel?f:c,u&&!r&&(r=setTimeout(()=>{n&&s(n),r=null,c(a())},u)),n=setTimeout(()=>{r&&s(r),r=null,c(a())},l)})}}function B1(e){return e}function ui(e){return Fa()?(Ba(e),!0):!1}function D1(e,t=200,n={}){return k1(F1(t,n),e)}function V1(e,t=200,n={}){const r=V(e.value),o=D1(()=>{r.value=e.value},t,n);return he(e,()=>o()),r}function j1(e,t=!0){Ge()?Ke(e):t?e():Ie(e)}function bu(e,t,n={}){const{immediate:r=!0}=n,o=V(!1);let s=null;function i(){s&&(clearTimeout(s),s=null)}function a(){o.value=!1,i()}function l(...u){i(),o.value=!0,s=setTimeout(()=>{o.value=!1,s=null,e(...u)},Ks(t))}return r&&(o.value=!0,st&&l()),ui(a),{isPending:Mr(o),start:l,stop:a}}function sr(e){var t;const n=Ks(e);return(t=n==null?void 0:n.$el)!=null?t:n}const ci=st?window:void 0,z1=st?window.document:void 0;function tn(...e){let t,n,r,o;if($1(e[0])||Array.isArray(e[0])?([n,r,o]=e,t=ci):[t,n,r,o]=e,!t)return Sa;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],i=()=>{s.forEach(c=>c()),s.length=0},a=(c,f,d,v)=>(c.addEventListener(f,d,v),()=>c.removeEventListener(f,d,v)),l=he(()=>[sr(t),Ks(o)],([c,f])=>{i(),c&&s.push(...n.flatMap(d=>r.map(v=>a(c,d,v,f))))},{immediate:!0,flush:"post"}),u=()=>{l(),i()};return ui(u),u}let pd=!1;function H1(e,t,n={}){const{window:r=ci,ignore:o=[],capture:s=!0,detectIframe:i=!1}=n;if(!r)return;gu&&!pd&&(pd=!0,Array.from(r.document.body.children).forEach(d=>d.addEventListener("click",Sa)));let a=!0;const l=d=>o.some(v=>{if(typeof v=="string")return Array.from(r.document.querySelectorAll(v)).some(p=>p===d.target||d.composedPath().includes(p));{const p=sr(v);return p&&(d.target===p||d.composedPath().includes(p))}}),c=[tn(r,"click",d=>{const v=sr(e);if(!(!v||v===d.target||d.composedPath().includes(v))){if(d.detail===0&&(a=!l(d)),!a){a=!0;return}t(d)}},{passive:!0,capture:s}),tn(r,"pointerdown",d=>{const v=sr(e);v&&(a=!d.composedPath().includes(v)&&!l(d))},{passive:!0}),i&&tn(r,"blur",d=>{var v;const p=sr(e);((v=r.document.activeElement)==null?void 0:v.tagName)==="IFRAME"&&!(p!=null&&p.contains(r.document.activeElement))&&t(d)})].filter(Boolean);return()=>c.forEach(d=>d())}function om(e,t=!1){const n=V(),r=()=>n.value=!!e();return r(),j1(r,t),n}const hd=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},vd="__vueuse_ssr_handlers__";hd[vd]=hd[vd]||{};function U1({document:e=z1}={}){if(!e)return V("visible");const t=V(e.visibilityState);return tn(e,"visibilitychange",()=>{t.value=e.visibilityState}),t}var md=Object.getOwnPropertySymbols,K1=Object.prototype.hasOwnProperty,q1=Object.prototype.propertyIsEnumerable,W1=(e,t)=>{var n={};for(var r in e)K1.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&md)for(var r of md(e))t.indexOf(r)<0&&q1.call(e,r)&&(n[r]=e[r]);return n};function Dt(e,t,n={}){const r=n,{window:o=ci}=r,s=W1(r,["window"]);let i;const a=om(()=>o&&"ResizeObserver"in o),l=()=>{i&&(i.disconnect(),i=void 0)},u=he(()=>sr(e),f=>{l(),a.value&&o&&f&&(i=new ResizeObserver(t),i.observe(f,s))},{immediate:!0,flush:"post"}),c=()=>{l(),u()};return ui(c),{isSupported:a,stop:c}}var gd=Object.getOwnPropertySymbols,G1=Object.prototype.hasOwnProperty,Y1=Object.prototype.propertyIsEnumerable,J1=(e,t)=>{var n={};for(var r in e)G1.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&gd)for(var r of gd(e))t.indexOf(r)<0&&Y1.call(e,r)&&(n[r]=e[r]);return n};function X1(e,t,n={}){const r=n,{window:o=ci}=r,s=J1(r,["window"]);let i;const a=om(()=>o&&"MutationObserver"in o),l=()=>{i&&(i.disconnect(),i=void 0)},u=he(()=>sr(e),f=>{l(),a.value&&o&&f&&(i=new MutationObserver(t),i.observe(f,s))},{immediate:!0}),c=()=>{l(),u()};return ui(c),{isSupported:a,stop:c}}var bd;(function(e){e.UP="UP",e.RIGHT="RIGHT",e.DOWN="DOWN",e.LEFT="LEFT",e.NONE="NONE"})(bd||(bd={}));var Z1=Object.defineProperty,yd=Object.getOwnPropertySymbols,Q1=Object.prototype.hasOwnProperty,eS=Object.prototype.propertyIsEnumerable,wd=(e,t,n)=>t in e?Z1(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,tS=(e,t)=>{for(var n in t||(t={}))Q1.call(t,n)&&wd(e,n,t[n]);if(yd)for(var n of yd(t))eS.call(t,n)&&wd(e,n,t[n]);return e};const nS={easeInSine:[.12,0,.39,0],easeOutSine:[.61,1,.88,1],easeInOutSine:[.37,0,.63,1],easeInQuad:[.11,0,.5,0],easeOutQuad:[.5,1,.89,1],easeInOutQuad:[.45,0,.55,1],easeInCubic:[.32,0,.67,0],easeOutCubic:[.33,1,.68,1],easeInOutCubic:[.65,0,.35,1],easeInQuart:[.5,0,.75,0],easeOutQuart:[.25,1,.5,1],easeInOutQuart:[.76,0,.24,1],easeInQuint:[.64,0,.78,0],easeOutQuint:[.22,1,.36,1],easeInOutQuint:[.83,0,.17,1],easeInExpo:[.7,0,.84,0],easeOutExpo:[.16,1,.3,1],easeInOutExpo:[.87,0,.13,1],easeInCirc:[.55,0,1,.45],easeOutCirc:[0,.55,.45,1],easeInOutCirc:[.85,0,.15,1],easeInBack:[.36,0,.66,-.56],easeOutBack:[.34,1.56,.64,1],easeInOutBack:[.68,-.6,.32,1.6]};tS({linear:B1},nS);function rS({window:e=ci}={}){if(!e)return V(!1);const t=V(e.document.hasFocus());return tn(e,"blur",()=>{t.value=!1}),tn(e,"focus",()=>{t.value=!0}),t}const oS=()=>st&&/firefox/i.test(window.navigator.userAgent);var sm=typeof global=="object"&&global&&global.Object===Object&&global,sS=typeof self=="object"&&self&&self.Object===Object&&self,Pn=sm||sS||Function("return this")(),fn=Pn.Symbol,im=Object.prototype,iS=im.hasOwnProperty,aS=im.toString,ps=fn?fn.toStringTag:void 0;function lS(e){var t=iS.call(e,ps),n=e[ps];try{e[ps]=void 0;var r=!0}catch{}var o=aS.call(e);return r&&(t?e[ps]=n:delete e[ps]),o}var uS=Object.prototype,cS=uS.toString;function fS(e){return cS.call(e)}var dS="[object Null]",pS="[object Undefined]",Sd=fn?fn.toStringTag:void 0;function Qo(e){return e==null?e===void 0?pS:dS:Sd&&Sd in Object(e)?lS(e):fS(e)}function $r(e){return e!=null&&typeof e=="object"}var hS="[object Symbol]";function Xa(e){return typeof e=="symbol"||$r(e)&&Qo(e)==hS}function vS(e,t){for(var n=-1,r=e==null?0:e.length,o=Array(r);++n0){if(++t>=HS)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}function WS(e){return function(){return e}}var Ea=function(){try{var e=yo(Object,"defineProperty");return e({},"",{}),e}catch{}}(),GS=Ea?function(e,t){return Ea(e,"toString",{configurable:!0,enumerable:!1,value:WS(t),writable:!0})}:lm,YS=qS(GS);function JS(e,t){for(var n=-1,r=e==null?0:e.length;++n-1&&e%1==0&&e-1&&e%1==0&&e<=rE}function fm(e){return e!=null&&mc(e.length)&&!um(e)}var oE=Object.prototype;function gc(e){var t=e&&e.constructor,n=typeof t=="function"&&t.prototype||oE;return e===n}function sE(e,t){for(var n=-1,r=Array(e);++n-1}function h2(e,t){var n=this.__data__,r=Qa(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this}function gr(e){var t=-1,n=e==null?0:e.length;for(this.clear();++ta))return!1;var u=s.get(e),c=s.get(t);if(u&&c)return u==t&&c==e;var f=-1,d=!0,v=n&vC?new Ta:void 0;for(s.set(e,t),s.set(t,e);++f=t||R<0||f&&N>=s}function m(){var A=kl();if(b(A))return S(A);a=setTimeout(m,h(A))}function S(A){return a=void 0,d&&r?v(A):(r=o=void 0,i)}function _(){a!==void 0&&clearTimeout(a),u=0,r=l=o=a=void 0}function w(){return a===void 0?i:S(kl())}function y(){var A=kl(),R=b(A);if(r=arguments,o=this,l=A,R){if(a===void 0)return p(l);if(f)return clearTimeout(a),a=setTimeout(m,t),v(l)}return a===void 0&&(a=setTimeout(m,t)),i}return y.cancel=_,y.flush=w,y}function rT(e,t,n){var r=e==null?0:e.length;if(!r)return-1;var o=r-1;return XS(e,ZC(t),o)}function Oa(e){for(var t=-1,n=e==null?0:e.length,r={};++te===void 0,Bt=e=>typeof e=="boolean",je=e=>typeof e=="number",ar=e=>typeof Element>"u"?!1:e instanceof Element,Cu=e=>qn(e),aT=e=>Ae(e)?!Number.isNaN(Number(e)):!1,lT=(e="")=>e.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&").replace(/-/g,"\\x2d"),Pr=e=>si(e),Zd=e=>Object.keys(e),Fl=(e,t,n)=>({get value(){return zn(e,t,n)},set value(r){iT(e,t,r)}});class uT extends Error{constructor(t){super(t),this.name="ElementPlusError"}}function Br(e,t){throw new uT(`[${e}] ${t}`)}const Pm=(e="")=>e.split(" ").filter(t=>!!t.trim()),Qd=(e,t)=>{if(!e||!t)return!1;if(t.includes(" "))throw new Error("className should not contain space.");return e.classList.contains(t)},Tu=(e,t)=>{!e||!t.trim()||e.classList.add(...Pm(t))},Gs=(e,t)=>{!e||!t.trim()||e.classList.remove(...Pm(t))},Po=(e,t)=>{var n;if(!st||!e||!t)return"";let r=Mt(t);r==="float"&&(r="cssFloat");try{const o=e.style[r];if(o)return o;const s=(n=document.defaultView)==null?void 0:n.getComputedStyle(e,"");return s?s[r]:""}catch{return e.style[r]}};function pn(e,t="px"){if(!e)return"";if(je(e)||aT(e))return`${e}${t}`;if(Ae(e))return e}let xi;const cT=e=>{var t;if(!st)return 0;if(xi!==void 0)return xi;const n=document.createElement("div");n.className=`${e}-scrollbar__wrap`,n.style.visibility="hidden",n.style.width="100px",n.style.position="absolute",n.style.top="-9999px",document.body.appendChild(n);const r=n.offsetWidth;n.style.overflow="scroll";const o=document.createElement("div");o.style.width="100%",n.appendChild(o);const s=o.offsetWidth;return(t=n.parentNode)==null||t.removeChild(n),xi=r-s,xi};function fT(e,t){if(!st)return;if(!t){e.scrollTop=0;return}const n=[];let r=t.offsetParent;for(;r!==null&&e!==r&&e.contains(r);)n.push(r),r=r.offsetParent;const o=t.offsetTop+n.reduce((l,u)=>l+u.offsetTop,0),s=o+t.offsetHeight,i=e.scrollTop,a=i+e.clientHeight;oa&&(e.scrollTop=s-e.clientHeight)}/*! Element Plus Icons Vue v2.3.2 */var dT=Z({name:"ArrowDown",__name:"arrow-down",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M831.872 340.864 512 652.672 192.128 340.864a30.59 30.59 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.59 30.59 0 0 0-42.752 0z"})]))}}),Nm=dT,pT=Z({name:"ArrowLeft",__name:"arrow-left",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.59 30.59 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.59 30.59 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0"})]))}}),hT=pT,vT=Z({name:"ArrowRight",__name:"arrow-right",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M340.864 149.312a30.59 30.59 0 0 0 0 42.752L652.736 512 340.864 831.872a30.59 30.59 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"})]))}}),mT=vT,gT=Z({name:"ArrowUp",__name:"arrow-up",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0"})]))}}),bT=gT,yT=Z({name:"CircleCheckFilled",__name:"circle-check-filled",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.27 38.27 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z"})]))}}),_N=yT,wT=Z({name:"CircleCheck",__name:"circle-check",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"}),le("path",{fill:"currentColor",d:"M745.344 361.344a32 32 0 0 1 45.312 45.312l-288 288a32 32 0 0 1-45.312 0l-160-160a32 32 0 1 1 45.312-45.312L480 626.752z"})]))}}),ST=wT,ET=Z({name:"CircleCloseFilled",__name:"circle-close-filled",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336z"})]))}}),Lm=ET,_T=Z({name:"CircleClose",__name:"circle-close",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"m466.752 512-90.496-90.496a32 32 0 0 1 45.248-45.248L512 466.752l90.496-90.496a32 32 0 1 1 45.248 45.248L557.248 512l90.496 90.496a32 32 0 1 1-45.248 45.248L512 557.248l-90.496 90.496a32 32 0 0 1-45.248-45.248z"}),le("path",{fill:"currentColor",d:"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768m0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896"})]))}}),Oc=_T,CT=Z({name:"Close",__name:"close",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"})]))}}),Ys=CT,TT=Z({name:"Delete",__name:"delete",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32zm448-64v-64H416v64zM224 896h576V256H224zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32m192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32"})]))}}),CN=TT,OT=Z({name:"Download",__name:"download",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64m384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64z"})]))}}),TN=OT,AT=Z({name:"Edit",__name:"edit",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M832 512a32 32 0 1 1 64 0v352a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h352a32 32 0 0 1 0 64H192v640h640z"}),le("path",{fill:"currentColor",d:"m469.952 554.24 52.8-7.552L847.104 222.4a32 32 0 1 0-45.248-45.248L477.44 501.44l-7.552 52.8zm422.4-422.4a96 96 0 0 1 0 135.808l-331.84 331.84a32 32 0 0 1-18.112 9.088L436.8 623.68a32 32 0 0 1-36.224-36.224l15.104-105.6a32 32 0 0 1 9.024-18.112l331.904-331.84a96 96 0 0 1 135.744 0z"})]))}}),ON=AT,RT=Z({name:"Folder",__name:"folder",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M128 192v640h768V320H485.76L357.504 192zm-32-64h287.872l128.384 128H928a32 32 0 0 1 32 32v576a32 32 0 0 1-32 32H96a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32"})]))}}),AN=RT,xT=Z({name:"Hide",__name:"hide",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M876.8 156.8c0-9.6-3.2-16-9.6-22.4s-12.8-9.6-22.4-9.6-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176S0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4s3.2 16 9.6 22.4 12.8 9.6 22.4 9.6 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4m-646.4 528Q115.2 579.2 76.8 512q43.2-72 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4m140.8-96Q352 555.2 352 512c0-44.8 16-83.2 48-112s67.2-48 112-48c28.8 0 54.4 6.4 73.6 19.2zM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6q-43.2 72-153.6 172.8c-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176S1024 528 1024 512s-48.001-73.6-134.401-176"}),le("path",{fill:"currentColor",d:"M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112s-67.2 48-112 48"})]))}}),IT=xT,PT=Z({name:"InfoFilled",__name:"info-filled",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896.064A448 448 0 0 1 512 64m67.2 275.072c33.28 0 60.288-23.104 60.288-57.344s-27.072-57.344-60.288-57.344c-33.28 0-60.16 23.104-60.16 57.344s26.88 57.344 60.16 57.344M590.912 699.2c0-6.848 2.368-24.64 1.024-34.752l-52.608 60.544c-10.88 11.456-24.512 19.392-30.912 17.28a12.99 12.99 0 0 1-8.256-14.72l87.68-276.992c7.168-35.136-12.544-67.2-54.336-71.296-44.096 0-108.992 44.736-148.48 101.504 0 6.784-1.28 23.68.064 33.792l52.544-60.608c10.88-11.328 23.552-19.328 29.952-17.152a12.8 12.8 0 0 1 7.808 16.128L388.48 728.576c-10.048 32.256 8.96 63.872 55.04 71.04 67.84 0 107.904-43.648 147.456-100.416z"})]))}}),Mm=PT,NT=Z({name:"Link",__name:"link",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M715.648 625.152 670.4 579.904l90.496-90.56c75.008-74.944 85.12-186.368 22.656-248.896-62.528-62.464-173.952-52.352-248.96 22.656L444.16 353.6l-45.248-45.248 90.496-90.496c100.032-99.968 251.968-110.08 339.456-22.656 87.488 87.488 77.312 239.424-22.656 339.456l-90.496 90.496zm-90.496 90.496-90.496 90.496C434.624 906.112 282.688 916.224 195.2 828.8c-87.488-87.488-77.312-239.424 22.656-339.456l90.496-90.496 45.248 45.248-90.496 90.56c-75.008 74.944-85.12 186.368-22.656 248.896 62.528 62.464 173.952 52.352 248.96-22.656l90.496-90.496zm0-362.048 45.248 45.248L398.848 670.4 353.6 625.152z"})]))}}),RN=NT,LT=Z({name:"Loading",__name:"loading",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32m0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32m448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32m-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32M195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248m452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248M828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0m-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0"})]))}}),Js=LT,MT=Z({name:"Minus",__name:"minus",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M128 544h768a32 32 0 1 0 0-64H128a32 32 0 0 0 0 64"})]))}}),$T=MT,kT=Z({name:"Plus",__name:"plus",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M480 480V128a32 32 0 0 1 64 0v352h352a32 32 0 1 1 0 64H544v352a32 32 0 1 1-64 0V544H128a32 32 0 0 1 0-64z"})]))}}),$m=kT,FT=Z({name:"Search",__name:"search",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704"})]))}}),xN=FT,BT=Z({name:"SuccessFilled",__name:"success-filled",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.27 38.27 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336z"})]))}}),km=BT,DT=Z({name:"View",__name:"view",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288m0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.19 160.19 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160"})]))}}),VT=DT,jT=Z({name:"WarningFilled",__name:"warning-filled",setup(e){return(t,n)=>(L(),ee("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 1024 1024"},[le("path",{fill:"currentColor",d:"M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 192a58.43 58.43 0 0 0-58.24 63.744l23.36 256.384a35.072 35.072 0 0 0 69.76 0l23.296-256.384A58.43 58.43 0 0 0 512 256m0 512a51.2 51.2 0 1 0 0-102.4 51.2 51.2 0 0 0 0 102.4"})]))}}),Fm=jT;const Bm="__epPropKey",we=e=>e,zT=e=>Oe(e)&&!!e[Bm],rl=(e,t)=>{if(!Oe(e)||zT(e))return e;const{values:n,required:r,default:o,type:s,validator:i}=e,l={type:s,required:!!r,validator:n||i?u=>{let c=!1,f=[];if(n&&(f=Array.from(n),Ve(e,"default")&&f.push(o),c||(c=f.includes(u))),i&&(c||(c=i(u))),!c&&f.length>0){const d=[...new Set(f)].map(v=>JSON.stringify(v)).join(", ");k0(`Invalid prop: validation failed${t?` for prop "${t}"`:""}. Expected one of [${d}], got value ${JSON.stringify(u)}.`)}return c}:void 0,[Bm]:!0};return Ve(e,"default")&&(l.default=o),l},xe=e=>Oa(Object.entries(e).map(([t,n])=>[t,rl(n,t)])),jt=we([String,Object,Function]),HT={Close:Ys},Dm={Close:Ys,SuccessFilled:km,InfoFilled:Mm,WarningFilled:Fm,CircleCloseFilled:Lm},Ra={success:km,warning:Fm,error:Lm,info:Mm},Vm={validating:Js,success:ST,error:Oc},yt=(e,t)=>{if(e.install=n=>{for(const r of[e,...Object.values(t??{})])n.component(r.name,r)},t)for(const[n,r]of Object.entries(t))e[n]=r;return e},UT=(e,t)=>(e.install=n=>{e._context=n._context,n.config.globalProperties[t]=e},e),KT=(e,t)=>(e.install=n=>{n.directive(t,e)},e),wo=e=>(e.install=ht,e),qT=(...e)=>t=>{e.forEach(n=>{ve(n)?n(t):n.value=t})},_n={tab:"Tab",enter:"Enter",space:"Space",left:"ArrowLeft",up:"ArrowUp",right:"ArrowRight",down:"ArrowDown",esc:"Escape",delete:"Delete",backspace:"Backspace"},rt="update:modelValue",fo="change",io="input",es=["","default","small","large"],jm=e=>["",...es].includes(e);var Zi=(e=>(e[e.TEXT=1]="TEXT",e[e.CLASS=2]="CLASS",e[e.STYLE=4]="STYLE",e[e.PROPS=8]="PROPS",e[e.FULL_PROPS=16]="FULL_PROPS",e[e.HYDRATE_EVENTS=32]="HYDRATE_EVENTS",e[e.STABLE_FRAGMENT=64]="STABLE_FRAGMENT",e[e.KEYED_FRAGMENT=128]="KEYED_FRAGMENT",e[e.UNKEYED_FRAGMENT=256]="UNKEYED_FRAGMENT",e[e.NEED_PATCH=512]="NEED_PATCH",e[e.DYNAMIC_SLOTS=1024]="DYNAMIC_SLOTS",e[e.HOISTED=-1]="HOISTED",e[e.BAIL=-2]="BAIL",e))(Zi||{});const Qi=e=>{const t=pe(e)?e:[e],n=[];return t.forEach(r=>{var o;pe(r)?n.push(...Qi(r)):cn(r)&&pe(r.children)?n.push(...Qi(r.children)):(n.push(r),cn(r)&&((o=r.component)!=null&&o.subTree)&&n.push(...Qi(r.component.subTree)))}),n},WT=e=>/([\uAC00-\uD7AF\u3130-\u318F])+/gi.test(e),ol=e=>e,GT=["class","style"],YT=/^on[A-Z]/,JT=(e={})=>{const{excludeListeners:t=!1,excludeKeys:n}=e,r=C(()=>((n==null?void 0:n.value)||[]).concat(GT)),o=Ge();return C(o?()=>{var s;return Oa(Object.entries((s=o.proxy)==null?void 0:s.$attrs).filter(([i])=>!r.value.includes(i)&&!(t&&YT.test(i))))}:()=>({}))},xs=({from:e,replacement:t,scope:n,version:r,ref:o,type:s="API"},i)=>{he(()=>g(i),a=>{},{immediate:!0})},zm=(e,t,n,r)=>{let o={offsetX:0,offsetY:0};const s=u=>{const c=u.clientX,f=u.clientY,{offsetX:d,offsetY:v}=o,p=e.value.getBoundingClientRect(),h=p.left,b=p.top,m=p.width,S=p.height,_=document.documentElement.clientWidth,w=document.documentElement.clientHeight,y=-h+d,A=-b+v,R=_-h-m+d,N=w-b-S+v,I=k=>{let $=d+k.clientX-c,F=v+k.clientY-f;r!=null&&r.value||($=Math.min(Math.max($,y),R),F=Math.min(Math.max(F,A),N)),o={offsetX:$,offsetY:F},e.value&&(e.value.style.transform=`translate(${pn($)}, ${pn(F)})`)},x=()=>{document.removeEventListener("mousemove",I),document.removeEventListener("mouseup",x)};document.addEventListener("mousemove",I),document.addEventListener("mouseup",x)},i=()=>{t.value&&e.value&&t.value.addEventListener("mousedown",s)},a=()=>{t.value&&e.value&&t.value.removeEventListener("mousedown",s)},l=()=>{o={offsetX:0,offsetY:0},e.value&&(e.value.style.transform="none")};return Ke(()=>{Ha(()=>{n.value?i():a()})}),St(()=>{a()}),{resetPosition:l}};var XT={name:"en",el:{breadcrumb:{label:"Breadcrumb"},colorpicker:{confirm:"OK",clear:"Clear",defaultLabel:"color picker",description:"current color is {color}. press enter to select a new color.",alphaLabel:"pick alpha value"},datepicker:{now:"Now",today:"Today",cancel:"Cancel",clear:"Clear",confirm:"OK",dateTablePrompt:"Use the arrow keys and enter to select the day of the month",monthTablePrompt:"Use the arrow keys and enter to select the month",yearTablePrompt:"Use the arrow keys and enter to select the year",selectedDate:"Selected date",selectDate:"Select date",selectTime:"Select time",startDate:"Start Date",startTime:"Start Time",endDate:"End Date",endTime:"End Time",prevYear:"Previous Year",nextYear:"Next Year",prevMonth:"Previous Month",nextMonth:"Next Month",year:"",month1:"January",month2:"February",month3:"March",month4:"April",month5:"May",month6:"June",month7:"July",month8:"August",month9:"September",month10:"October",month11:"November",month12:"December",week:"week",weeks:{sun:"Sun",mon:"Mon",tue:"Tue",wed:"Wed",thu:"Thu",fri:"Fri",sat:"Sat"},weeksFull:{sun:"Sunday",mon:"Monday",tue:"Tuesday",wed:"Wednesday",thu:"Thursday",fri:"Friday",sat:"Saturday"},months:{jan:"Jan",feb:"Feb",mar:"Mar",apr:"Apr",may:"May",jun:"Jun",jul:"Jul",aug:"Aug",sep:"Sep",oct:"Oct",nov:"Nov",dec:"Dec"}},inputNumber:{decrease:"decrease number",increase:"increase number"},select:{loading:"Loading",noMatch:"No matching data",noData:"No data",placeholder:"Select"},mention:{loading:"Loading"},dropdown:{toggleDropdown:"Toggle Dropdown"},cascader:{noMatch:"No matching data",loading:"Loading",placeholder:"Select",noData:"No data"},pagination:{goto:"Go to",pagesize:"/page",total:"Total {total}",pageClassifier:"",page:"Page",prev:"Go to previous page",next:"Go to next page",currentPage:"page {pager}",prevPages:"Previous {pager} pages",nextPages:"Next {pager} pages",deprecationWarning:"Deprecated usages detected, please refer to the el-pagination documentation for more details"},dialog:{close:"Close this dialog"},drawer:{close:"Close this dialog"},messagebox:{title:"Message",confirm:"OK",cancel:"Cancel",error:"Illegal input",close:"Close this dialog"},upload:{deleteTip:"press delete to remove",delete:"Delete",preview:"Preview",continue:"Continue"},slider:{defaultLabel:"slider between {min} and {max}",defaultRangeStartLabel:"pick start value",defaultRangeEndLabel:"pick end value"},table:{emptyText:"No Data",confirmFilter:"Confirm",resetFilter:"Reset",clearFilter:"All",sumText:"Sum"},tour:{next:"Next",previous:"Previous",finish:"Finish"},tree:{emptyText:"No Data"},transfer:{noMatch:"No matching data",noData:"No data",titles:["List 1","List 2"],filterPlaceholder:"Enter keyword",noCheckedFormat:"{total} items",hasCheckedFormat:"{checked}/{total} checked"},image:{error:"FAILED"},pageHeader:{title:"Back"},popconfirm:{confirmButtonText:"Yes",cancelButtonText:"No"},carousel:{leftArrow:"Carousel arrow left",rightArrow:"Carousel arrow right",indicator:"Carousel switch to index {index}"}}};const ZT=e=>(t,n)=>QT(t,n,g(e)),QT=(e,t,n)=>zn(n,e,e).replace(/\{(\w+)\}/g,(r,o)=>{var s;return`${(s=t==null?void 0:t[o])!=null?s:`{${o}}`}`}),eO=e=>({lang:C(()=>g(e).name),locale:Ue(e)?e:V(e),t:ZT(e)}),Hm=Symbol("localeContextKey"),sl=e=>{const t=e||Ee(Hm,V());return eO(C(()=>t.value||XT))},Is="el",tO="is-",Wr=(e,t,n,r,o)=>{let s=`${e}-${t}`;return n&&(s+=`-${n}`),r&&(s+=`__${r}`),o&&(s+=`--${o}`),s},Um=Symbol("namespaceContextKey"),Ac=e=>{const t=e||(Ge()?Ee(Um,V(Is)):V(Is));return C(()=>g(t)||Is)},$e=(e,t)=>{const n=Ac(t);return{namespace:n,b:(h="")=>Wr(n.value,e,h,"",""),e:h=>h?Wr(n.value,e,"",h,""):"",m:h=>h?Wr(n.value,e,"","",h):"",be:(h,b)=>h&&b?Wr(n.value,e,h,b,""):"",em:(h,b)=>h&&b?Wr(n.value,e,"",h,b):"",bm:(h,b)=>h&&b?Wr(n.value,e,h,"",b):"",bem:(h,b,m)=>h&&b&&m?Wr(n.value,e,h,b,m):"",is:(h,...b)=>{const m=b.length>=1?b[0]:!0;return h&&m?`${tO}${h}`:""},cssVar:h=>{const b={};for(const m in h)h[m]&&(b[`--${n.value}-${m}`]=h[m]);return b},cssVarName:h=>`--${n.value}-${h}`,cssVarBlock:h=>{const b={};for(const m in h)h[m]&&(b[`--${n.value}-${e}-${m}`]=h[m]);return b},cssVarBlockName:h=>`--${n.value}-${e}-${h}`}},Km=(e,t={})=>{Ue(e)||Br("[useLockscreen]","You need to pass a ref param to this function");const n=t.ns||$e("popup"),r=C(()=>n.bm("parent","hidden"));if(!st||Qd(document.body,r.value))return;let o=0,s=!1,i="0";const a=()=>{setTimeout(()=>{typeof document>"u"||(Gs(document==null?void 0:document.body,r.value),s&&document&&(document.body.style.width=i))},200)};he(e,l=>{if(!l){a();return}s=!Qd(document.body,r.value),s&&(i=document.body.style.width),o=cT(n.namespace.value);const u=document.documentElement.clientHeight0&&(u||c==="scroll")&&s&&(document.body.style.width=`calc(100% - ${o}px)`),Tu(document.body,r.value)}),Ba(()=>a())},nO=rl({type:we(Boolean),default:null}),rO=rl({type:we(Function)}),oO=e=>{const t=`update:${e}`,n=`onUpdate:${e}`,r=[t],o={[e]:nO,[n]:rO};return{useModelToggle:({indicator:i,toggleReason:a,shouldHideWhenRouteChanges:l,shouldProceed:u,onShow:c,onHide:f})=>{const d=Ge(),{emit:v}=d,p=d.props,h=C(()=>ve(p[n])),b=C(()=>p[e]===null),m=R=>{i.value!==!0&&(i.value=!0,a&&(a.value=R),ve(c)&&c(R))},S=R=>{i.value!==!1&&(i.value=!1,a&&(a.value=R),ve(f)&&f(R))},_=R=>{if(p.disabled===!0||ve(u)&&!u())return;const N=h.value&&st;N&&v(t,!0),(b.value||!N)&&m(R)},w=R=>{if(p.disabled===!0||!st)return;const N=h.value&&st;N&&v(t,!1),(b.value||!N)&&S(R)},y=R=>{Bt(R)&&(p.disabled&&R?h.value&&v(t,!1):i.value!==R&&(R?m():S()))},A=()=>{i.value?w():_()};return he(()=>p[e],y),l&&d.appContext.config.globalProperties.$route!==void 0&&he(()=>({...d.proxy.$route}),()=>{l.value&&i.value&&w()}),Ke(()=>{y(p[e])}),{hide:w,show:_,toggle:A,hasUpdateHandler:h}},useModelToggleProps:o,useModelToggleEmits:r}},qm=e=>{const t=Ge();return C(()=>{var n,r;return(r=(n=t==null?void 0:t.proxy)==null?void 0:n.$props)==null?void 0:r[e]})};var zt="top",hn="bottom",vn="right",Ht="left",Rc="auto",di=[zt,hn,vn,Ht],Uo="start",Xs="end",sO="clippingParents",Wm="viewport",hs="popper",iO="reference",ep=di.reduce(function(e,t){return e.concat([t+"-"+Uo,t+"-"+Xs])},[]),il=[].concat(di,[Rc]).reduce(function(e,t){return e.concat([t,t+"-"+Uo,t+"-"+Xs])},[]),aO="beforeRead",lO="read",uO="afterRead",cO="beforeMain",fO="main",dO="afterMain",pO="beforeWrite",hO="write",vO="afterWrite",mO=[aO,lO,uO,cO,fO,dO,pO,hO,vO];function Gn(e){return e?(e.nodeName||"").toLowerCase():null}function nn(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}function po(e){var t=nn(e).Element;return e instanceof t||e instanceof Element}function un(e){var t=nn(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}function xc(e){if(typeof ShadowRoot>"u")return!1;var t=nn(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}function gO(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var r=t.styles[n]||{},o=t.attributes[n]||{},s=t.elements[n];!un(s)||!Gn(s)||(Object.assign(s.style,r),Object.keys(o).forEach(function(i){var a=o[i];a===!1?s.removeAttribute(i):s.setAttribute(i,a===!0?"":a)}))})}function bO(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(r){var o=t.elements[r],s=t.attributes[r]||{},i=Object.keys(t.styles.hasOwnProperty(r)?t.styles[r]:n[r]),a=i.reduce(function(l,u){return l[u]="",l},{});!un(o)||!Gn(o)||(Object.assign(o.style,a),Object.keys(s).forEach(function(l){o.removeAttribute(l)}))})}}var Gm={name:"applyStyles",enabled:!0,phase:"write",fn:gO,effect:bO,requires:["computeStyles"]};function Wn(e){return e.split("-")[0]}var ao=Math.max,xa=Math.min,Ko=Math.round;function Ou(){var e=navigator.userAgentData;return e!=null&&e.brands&&Array.isArray(e.brands)?e.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function Ym(){return!/^((?!chrome|android).)*safari/i.test(Ou())}function qo(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!1);var r=e.getBoundingClientRect(),o=1,s=1;t&&un(e)&&(o=e.offsetWidth>0&&Ko(r.width)/e.offsetWidth||1,s=e.offsetHeight>0&&Ko(r.height)/e.offsetHeight||1);var i=po(e)?nn(e):window,a=i.visualViewport,l=!Ym()&&n,u=(r.left+(l&&a?a.offsetLeft:0))/o,c=(r.top+(l&&a?a.offsetTop:0))/s,f=r.width/o,d=r.height/s;return{width:f,height:d,top:c,right:u+f,bottom:c+d,left:u,x:u,y:c}}function Ic(e){var t=qo(e),n=e.offsetWidth,r=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-r)<=1&&(r=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:r}}function Jm(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&xc(n)){var r=t;do{if(r&&e.isSameNode(r))return!0;r=r.parentNode||r.host}while(r)}return!1}function dr(e){return nn(e).getComputedStyle(e)}function yO(e){return["table","td","th"].indexOf(Gn(e))>=0}function Dr(e){return((po(e)?e.ownerDocument:e.document)||window.document).documentElement}function al(e){return Gn(e)==="html"?e:e.assignedSlot||e.parentNode||(xc(e)?e.host:null)||Dr(e)}function tp(e){return!un(e)||dr(e).position==="fixed"?null:e.offsetParent}function wO(e){var t=/firefox/i.test(Ou()),n=/Trident/i.test(Ou());if(n&&un(e)){var r=dr(e);if(r.position==="fixed")return null}var o=al(e);for(xc(o)&&(o=o.host);un(o)&&["html","body"].indexOf(Gn(o))<0;){var s=dr(o);if(s.transform!=="none"||s.perspective!=="none"||s.contain==="paint"||["transform","perspective"].indexOf(s.willChange)!==-1||t&&s.willChange==="filter"||t&&s.filter&&s.filter!=="none")return o;o=o.parentNode}return null}function pi(e){for(var t=nn(e),n=tp(e);n&&yO(n)&&dr(n).position==="static";)n=tp(n);return n&&(Gn(n)==="html"||Gn(n)==="body"&&dr(n).position==="static")?t:n||wO(e)||t}function Pc(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}function Ps(e,t,n){return ao(e,xa(t,n))}function SO(e,t,n){var r=Ps(e,t,n);return r>n?n:r}function Xm(){return{top:0,right:0,bottom:0,left:0}}function Zm(e){return Object.assign({},Xm(),e)}function Qm(e,t){return t.reduce(function(n,r){return n[r]=e,n},{})}var EO=function(e,t){return e=typeof e=="function"?e(Object.assign({},t.rects,{placement:t.placement})):e,Zm(typeof e!="number"?e:Qm(e,di))};function _O(e){var t,n=e.state,r=e.name,o=e.options,s=n.elements.arrow,i=n.modifiersData.popperOffsets,a=Wn(n.placement),l=Pc(a),u=[Ht,vn].indexOf(a)>=0,c=u?"height":"width";if(!(!s||!i)){var f=EO(o.padding,n),d=Ic(s),v=l==="y"?zt:Ht,p=l==="y"?hn:vn,h=n.rects.reference[c]+n.rects.reference[l]-i[l]-n.rects.popper[c],b=i[l]-n.rects.reference[l],m=pi(s),S=m?l==="y"?m.clientHeight||0:m.clientWidth||0:0,_=h/2-b/2,w=f[v],y=S-d[c]-f[p],A=S/2-d[c]/2+_,R=Ps(w,A,y),N=l;n.modifiersData[r]=(t={},t[N]=R,t.centerOffset=R-A,t)}}function CO(e){var t=e.state,n=e.options,r=n.element,o=r===void 0?"[data-popper-arrow]":r;o!=null&&(typeof o=="string"&&(o=t.elements.popper.querySelector(o),!o)||Jm(t.elements.popper,o)&&(t.elements.arrow=o))}var TO={name:"arrow",enabled:!0,phase:"main",fn:_O,effect:CO,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Wo(e){return e.split("-")[1]}var OO={top:"auto",right:"auto",bottom:"auto",left:"auto"};function AO(e,t){var n=e.x,r=e.y,o=t.devicePixelRatio||1;return{x:Ko(n*o)/o||0,y:Ko(r*o)/o||0}}function np(e){var t,n=e.popper,r=e.popperRect,o=e.placement,s=e.variation,i=e.offsets,a=e.position,l=e.gpuAcceleration,u=e.adaptive,c=e.roundOffsets,f=e.isFixed,d=i.x,v=d===void 0?0:d,p=i.y,h=p===void 0?0:p,b=typeof c=="function"?c({x:v,y:h}):{x:v,y:h};v=b.x,h=b.y;var m=i.hasOwnProperty("x"),S=i.hasOwnProperty("y"),_=Ht,w=zt,y=window;if(u){var A=pi(n),R="clientHeight",N="clientWidth";if(A===nn(n)&&(A=Dr(n),dr(A).position!=="static"&&a==="absolute"&&(R="scrollHeight",N="scrollWidth")),A=A,o===zt||(o===Ht||o===vn)&&s===Xs){w=hn;var I=f&&A===y&&y.visualViewport?y.visualViewport.height:A[R];h-=I-r.height,h*=l?1:-1}if(o===Ht||(o===zt||o===hn)&&s===Xs){_=vn;var x=f&&A===y&&y.visualViewport?y.visualViewport.width:A[N];v-=x-r.width,v*=l?1:-1}}var k=Object.assign({position:a},u&&OO),$=c===!0?AO({x:v,y:h},nn(n)):{x:v,y:h};if(v=$.x,h=$.y,l){var F;return Object.assign({},k,(F={},F[w]=S?"0":"",F[_]=m?"0":"",F.transform=(y.devicePixelRatio||1)<=1?"translate("+v+"px, "+h+"px)":"translate3d("+v+"px, "+h+"px, 0)",F))}return Object.assign({},k,(t={},t[w]=S?h+"px":"",t[_]=m?v+"px":"",t.transform="",t))}function RO(e){var t=e.state,n=e.options,r=n.gpuAcceleration,o=r===void 0?!0:r,s=n.adaptive,i=s===void 0?!0:s,a=n.roundOffsets,l=a===void 0?!0:a,u={placement:Wn(t.placement),variation:Wo(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:o,isFixed:t.options.strategy==="fixed"};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,np(Object.assign({},u,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:i,roundOffsets:l})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,np(Object.assign({},u,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}var eg={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:RO,data:{}},Ii={passive:!0};function xO(e){var t=e.state,n=e.instance,r=e.options,o=r.scroll,s=o===void 0?!0:o,i=r.resize,a=i===void 0?!0:i,l=nn(t.elements.popper),u=[].concat(t.scrollParents.reference,t.scrollParents.popper);return s&&u.forEach(function(c){c.addEventListener("scroll",n.update,Ii)}),a&&l.addEventListener("resize",n.update,Ii),function(){s&&u.forEach(function(c){c.removeEventListener("scroll",n.update,Ii)}),a&&l.removeEventListener("resize",n.update,Ii)}}var tg={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:xO,data:{}},IO={left:"right",right:"left",bottom:"top",top:"bottom"};function ea(e){return e.replace(/left|right|bottom|top/g,function(t){return IO[t]})}var PO={start:"end",end:"start"};function rp(e){return e.replace(/start|end/g,function(t){return PO[t]})}function Nc(e){var t=nn(e),n=t.pageXOffset,r=t.pageYOffset;return{scrollLeft:n,scrollTop:r}}function Lc(e){return qo(Dr(e)).left+Nc(e).scrollLeft}function NO(e,t){var n=nn(e),r=Dr(e),o=n.visualViewport,s=r.clientWidth,i=r.clientHeight,a=0,l=0;if(o){s=o.width,i=o.height;var u=Ym();(u||!u&&t==="fixed")&&(a=o.offsetLeft,l=o.offsetTop)}return{width:s,height:i,x:a+Lc(e),y:l}}function LO(e){var t,n=Dr(e),r=Nc(e),o=(t=e.ownerDocument)==null?void 0:t.body,s=ao(n.scrollWidth,n.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),i=ao(n.scrollHeight,n.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),a=-r.scrollLeft+Lc(e),l=-r.scrollTop;return dr(o||n).direction==="rtl"&&(a+=ao(n.clientWidth,o?o.clientWidth:0)-s),{width:s,height:i,x:a,y:l}}function Mc(e){var t=dr(e),n=t.overflow,r=t.overflowX,o=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+o+r)}function ng(e){return["html","body","#document"].indexOf(Gn(e))>=0?e.ownerDocument.body:un(e)&&Mc(e)?e:ng(al(e))}function Ns(e,t){var n;t===void 0&&(t=[]);var r=ng(e),o=r===((n=e.ownerDocument)==null?void 0:n.body),s=nn(r),i=o?[s].concat(s.visualViewport||[],Mc(r)?r:[]):r,a=t.concat(i);return o?a:a.concat(Ns(al(i)))}function Au(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}function MO(e,t){var n=qo(e,!1,t==="fixed");return n.top=n.top+e.clientTop,n.left=n.left+e.clientLeft,n.bottom=n.top+e.clientHeight,n.right=n.left+e.clientWidth,n.width=e.clientWidth,n.height=e.clientHeight,n.x=n.left,n.y=n.top,n}function op(e,t,n){return t===Wm?Au(NO(e,n)):po(t)?MO(t,n):Au(LO(Dr(e)))}function $O(e){var t=Ns(al(e)),n=["absolute","fixed"].indexOf(dr(e).position)>=0,r=n&&un(e)?pi(e):e;return po(r)?t.filter(function(o){return po(o)&&Jm(o,r)&&Gn(o)!=="body"}):[]}function kO(e,t,n,r){var o=t==="clippingParents"?$O(e):[].concat(t),s=[].concat(o,[n]),i=s[0],a=s.reduce(function(l,u){var c=op(e,u,r);return l.top=ao(c.top,l.top),l.right=xa(c.right,l.right),l.bottom=xa(c.bottom,l.bottom),l.left=ao(c.left,l.left),l},op(e,i,r));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function rg(e){var t=e.reference,n=e.element,r=e.placement,o=r?Wn(r):null,s=r?Wo(r):null,i=t.x+t.width/2-n.width/2,a=t.y+t.height/2-n.height/2,l;switch(o){case zt:l={x:i,y:t.y-n.height};break;case hn:l={x:i,y:t.y+t.height};break;case vn:l={x:t.x+t.width,y:a};break;case Ht:l={x:t.x-n.width,y:a};break;default:l={x:t.x,y:t.y}}var u=o?Pc(o):null;if(u!=null){var c=u==="y"?"height":"width";switch(s){case Uo:l[u]=l[u]-(t[c]/2-n[c]/2);break;case Xs:l[u]=l[u]+(t[c]/2-n[c]/2);break}}return l}function Zs(e,t){t===void 0&&(t={});var n=t,r=n.placement,o=r===void 0?e.placement:r,s=n.strategy,i=s===void 0?e.strategy:s,a=n.boundary,l=a===void 0?sO:a,u=n.rootBoundary,c=u===void 0?Wm:u,f=n.elementContext,d=f===void 0?hs:f,v=n.altBoundary,p=v===void 0?!1:v,h=n.padding,b=h===void 0?0:h,m=Zm(typeof b!="number"?b:Qm(b,di)),S=d===hs?iO:hs,_=e.rects.popper,w=e.elements[p?S:d],y=kO(po(w)?w:w.contextElement||Dr(e.elements.popper),l,c,i),A=qo(e.elements.reference),R=rg({reference:A,element:_,placement:o}),N=Au(Object.assign({},_,R)),I=d===hs?N:A,x={top:y.top-I.top+m.top,bottom:I.bottom-y.bottom+m.bottom,left:y.left-I.left+m.left,right:I.right-y.right+m.right},k=e.modifiersData.offset;if(d===hs&&k){var $=k[o];Object.keys(x).forEach(function(F){var Y=[vn,hn].indexOf(F)>=0?1:-1,P=[zt,hn].indexOf(F)>=0?"y":"x";x[F]+=$[P]*Y})}return x}function FO(e,t){t===void 0&&(t={});var n=t,r=n.placement,o=n.boundary,s=n.rootBoundary,i=n.padding,a=n.flipVariations,l=n.allowedAutoPlacements,u=l===void 0?il:l,c=Wo(r),f=c?a?ep:ep.filter(function(p){return Wo(p)===c}):di,d=f.filter(function(p){return u.indexOf(p)>=0});d.length===0&&(d=f);var v=d.reduce(function(p,h){return p[h]=Zs(e,{placement:h,boundary:o,rootBoundary:s,padding:i})[Wn(h)],p},{});return Object.keys(v).sort(function(p,h){return v[p]-v[h]})}function BO(e){if(Wn(e)===Rc)return[];var t=ea(e);return[rp(e),t,rp(t)]}function DO(e){var t=e.state,n=e.options,r=e.name;if(!t.modifiersData[r]._skip){for(var o=n.mainAxis,s=o===void 0?!0:o,i=n.altAxis,a=i===void 0?!0:i,l=n.fallbackPlacements,u=n.padding,c=n.boundary,f=n.rootBoundary,d=n.altBoundary,v=n.flipVariations,p=v===void 0?!0:v,h=n.allowedAutoPlacements,b=t.options.placement,m=Wn(b),S=m===b,_=l||(S||!p?[ea(b)]:BO(b)),w=[b].concat(_).reduce(function(qe,ze){return qe.concat(Wn(ze)===Rc?FO(t,{placement:ze,boundary:c,rootBoundary:f,padding:u,flipVariations:p,allowedAutoPlacements:h}):ze)},[]),y=t.rects.reference,A=t.rects.popper,R=new Map,N=!0,I=w[0],x=0;x=0,P=Y?"width":"height",O=Zs(t,{placement:k,boundary:c,rootBoundary:f,altBoundary:d,padding:u}),j=Y?F?vn:Ht:F?hn:zt;y[P]>A[P]&&(j=ea(j));var Q=ea(j),me=[];if(s&&me.push(O[$]<=0),a&&me.push(O[j]<=0,O[Q]<=0),me.every(function(qe){return qe})){I=k,N=!1;break}R.set(k,me)}if(N)for(var Pe=p?3:1,Re=function(qe){var ze=w.find(function(De){var B=R.get(De);if(B)return B.slice(0,qe).every(function(K){return K})});if(ze)return I=ze,"break"},Ce=Pe;Ce>0;Ce--){var _e=Re(Ce);if(_e==="break")break}t.placement!==I&&(t.modifiersData[r]._skip=!0,t.placement=I,t.reset=!0)}}var VO={name:"flip",enabled:!0,phase:"main",fn:DO,requiresIfExists:["offset"],data:{_skip:!1}};function sp(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}function ip(e){return[zt,vn,hn,Ht].some(function(t){return e[t]>=0})}function jO(e){var t=e.state,n=e.name,r=t.rects.reference,o=t.rects.popper,s=t.modifiersData.preventOverflow,i=Zs(t,{elementContext:"reference"}),a=Zs(t,{altBoundary:!0}),l=sp(i,r),u=sp(a,o,s),c=ip(l),f=ip(u);t.modifiersData[n]={referenceClippingOffsets:l,popperEscapeOffsets:u,isReferenceHidden:c,hasPopperEscaped:f},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":c,"data-popper-escaped":f})}var zO={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:jO};function HO(e,t,n){var r=Wn(e),o=[Ht,zt].indexOf(r)>=0?-1:1,s=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,i=s[0],a=s[1];return i=i||0,a=(a||0)*o,[Ht,vn].indexOf(r)>=0?{x:a,y:i}:{x:i,y:a}}function UO(e){var t=e.state,n=e.options,r=e.name,o=n.offset,s=o===void 0?[0,0]:o,i=il.reduce(function(c,f){return c[f]=HO(f,t.rects,s),c},{}),a=i[t.placement],l=a.x,u=a.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=l,t.modifiersData.popperOffsets.y+=u),t.modifiersData[r]=i}var KO={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:UO};function qO(e){var t=e.state,n=e.name;t.modifiersData[n]=rg({reference:t.rects.reference,element:t.rects.popper,placement:t.placement})}var og={name:"popperOffsets",enabled:!0,phase:"read",fn:qO,data:{}};function WO(e){return e==="x"?"y":"x"}function GO(e){var t=e.state,n=e.options,r=e.name,o=n.mainAxis,s=o===void 0?!0:o,i=n.altAxis,a=i===void 0?!1:i,l=n.boundary,u=n.rootBoundary,c=n.altBoundary,f=n.padding,d=n.tether,v=d===void 0?!0:d,p=n.tetherOffset,h=p===void 0?0:p,b=Zs(t,{boundary:l,rootBoundary:u,padding:f,altBoundary:c}),m=Wn(t.placement),S=Wo(t.placement),_=!S,w=Pc(m),y=WO(w),A=t.modifiersData.popperOffsets,R=t.rects.reference,N=t.rects.popper,I=typeof h=="function"?h(Object.assign({},t.rects,{placement:t.placement})):h,x=typeof I=="number"?{mainAxis:I,altAxis:I}:Object.assign({mainAxis:0,altAxis:0},I),k=t.modifiersData.offset?t.modifiersData.offset[t.placement]:null,$={x:0,y:0};if(A){if(s){var F,Y=w==="y"?zt:Ht,P=w==="y"?hn:vn,O=w==="y"?"height":"width",j=A[w],Q=j+b[Y],me=j-b[P],Pe=v?-N[O]/2:0,Re=S===Uo?R[O]:N[O],Ce=S===Uo?-N[O]:-R[O],_e=t.elements.arrow,qe=v&&_e?Ic(_e):{width:0,height:0},ze=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:Xm(),De=ze[Y],B=ze[P],K=Ps(0,R[O],qe[O]),J=_?R[O]/2-Pe-K-De-x.mainAxis:Re-K-De-x.mainAxis,oe=_?-R[O]/2+Pe+K+B+x.mainAxis:Ce+K+B+x.mainAxis,ge=t.elements.arrow&&pi(t.elements.arrow),E=ge?w==="y"?ge.clientTop||0:ge.clientLeft||0:0,T=(F=k==null?void 0:k[w])!=null?F:0,M=j+J-T-E,W=j+oe-T,G=Ps(v?xa(Q,M):Q,j,v?ao(me,W):me);A[w]=G,$[w]=G-j}if(a){var q,se=w==="x"?zt:Ht,ne=w==="x"?hn:vn,te=A[y],X=y==="y"?"height":"width",Se=te+b[se],ue=te-b[ne],ye=[zt,Ht].indexOf(m)!==-1,z=(q=k==null?void 0:k[y])!=null?q:0,be=ye?Se:te-R[X]-N[X]-z+x.altAxis,Le=ye?te+R[X]+N[X]-z-x.altAxis:ue,ke=v&&ye?SO(be,te,Le):Ps(v?be:Se,te,v?Le:ue);A[y]=ke,$[y]=ke-te}t.modifiersData[r]=$}}var YO={name:"preventOverflow",enabled:!0,phase:"main",fn:GO,requiresIfExists:["offset"]};function JO(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}function XO(e){return e===nn(e)||!un(e)?Nc(e):JO(e)}function ZO(e){var t=e.getBoundingClientRect(),n=Ko(t.width)/e.offsetWidth||1,r=Ko(t.height)/e.offsetHeight||1;return n!==1||r!==1}function QO(e,t,n){n===void 0&&(n=!1);var r=un(t),o=un(t)&&ZO(t),s=Dr(t),i=qo(e,o,n),a={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(r||!r&&!n)&&((Gn(t)!=="body"||Mc(s))&&(a=XO(t)),un(t)?(l=qo(t,!0),l.x+=t.clientLeft,l.y+=t.clientTop):s&&(l.x=Lc(s))),{x:i.left+a.scrollLeft-l.x,y:i.top+a.scrollTop-l.y,width:i.width,height:i.height}}function eA(e){var t=new Map,n=new Set,r=[];e.forEach(function(s){t.set(s.name,s)});function o(s){n.add(s.name);var i=[].concat(s.requires||[],s.requiresIfExists||[]);i.forEach(function(a){if(!n.has(a)){var l=t.get(a);l&&o(l)}}),r.push(s)}return e.forEach(function(s){n.has(s.name)||o(s)}),r}function tA(e){var t=eA(e);return mO.reduce(function(n,r){return n.concat(t.filter(function(o){return o.phase===r}))},[])}function nA(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}function rA(e){var t=e.reduce(function(n,r){var o=n[r.name];return n[r.name]=o?Object.assign({},o,r,{options:Object.assign({},o.options,r.options),data:Object.assign({},o.data,r.data)}):r,n},{});return Object.keys(t).map(function(n){return t[n]})}var ap={placement:"bottom",modifiers:[],strategy:"absolute"};function lp(){for(var e=arguments.length,t=new Array(e),n=0;n{const r={name:"updateState",enabled:!0,phase:"write",fn:({state:l})=>{const u=lA(l);Object.assign(i.value,u)},requires:["computeStyles"]},o=C(()=>{const{onFirstUpdate:l,placement:u,strategy:c,modifiers:f}=g(n);return{onFirstUpdate:l,placement:u||"bottom",strategy:c||"absolute",modifiers:[...f||[],r,{name:"applyStyles",enabled:!1}]}}),s=ir(),i=V({styles:{popper:{position:g(o).strategy,left:"0",top:"0"},arrow:{position:"absolute"}},attributes:{}}),a=()=>{s.value&&(s.value.destroy(),s.value=void 0)};return he(o,l=>{const u=g(s);u&&u.setOptions(l)},{deep:!0}),he([e,t],([l,u])=>{a(),!(!l||!u)&&(s.value=iA(l,u,g(o)))}),St(()=>{a()}),{state:C(()=>{var l;return{...((l=g(s))==null?void 0:l.state)||{}}}),styles:C(()=>g(i).styles),attributes:C(()=>g(i).attributes),update:()=>{var l;return(l=g(s))==null?void 0:l.update()},forceUpdate:()=>{var l;return(l=g(s))==null?void 0:l.forceUpdate()},instanceRef:C(()=>g(s))}};function lA(e){const t=Object.keys(e.elements),n=Oa(t.map(o=>[o,e.styles[o]||{}])),r=Oa(t.map(o=>[o,e.attributes[o]]));return{styles:n,attributes:r}}const kc=e=>{if(!e)return{onClick:ht,onMousedown:ht,onMouseup:ht};let t=!1,n=!1;return{onClick:i=>{t&&n&&e(i),t=n=!1},onMousedown:i=>{t=i.target===i.currentTarget},onMouseup:i=>{n=i.target===i.currentTarget}}};function up(){let e;const t=(r,o)=>{n(),e=window.setTimeout(r,o)},n=()=>window.clearTimeout(e);return ui(()=>n()),{registerTimeout:t,cancelTimeout:n}}const cp={prefix:Math.floor(Math.random()*1e4),current:0},uA=Symbol("elIdInjection"),sg=()=>Ge()?Ee(uA,cp):cp,pr=e=>{const t=sg(),n=Ac();return C(()=>g(e)||`${n.value}-id-${t.prefix}-${t.current++}`)};let No=[];const fp=e=>{const t=e;t.key===_n.esc&&No.forEach(n=>n(t))},cA=e=>{Ke(()=>{No.length===0&&document.addEventListener("keydown",fp),st&&No.push(e)}),St(()=>{No=No.filter(t=>t!==e),No.length===0&&st&&document.removeEventListener("keydown",fp)})},ig=()=>{const e=Ac(),t=sg(),n=C(()=>`${e.value}-popper-container-${t.prefix}`),r=C(()=>`#${n.value}`);return{id:n,selector:r}},fA=e=>{const t=document.createElement("div");return t.id=e,document.body.appendChild(t),t},dA=()=>{const{id:e,selector:t}=ig();return sc(()=>{st&&(document.body.querySelector(t.value)||fA(e.value))}),{id:e,selector:t}},pA=xe({showAfter:{type:Number,default:0},hideAfter:{type:Number,default:200},autoClose:{type:Number,default:0}}),hA=({showAfter:e,hideAfter:t,autoClose:n,open:r,close:o})=>{const{registerTimeout:s}=up(),{registerTimeout:i,cancelTimeout:a}=up();return{onOpen:c=>{s(()=>{r(c);const f=g(n);je(f)&&f>0&&i(()=>{o(c)},f)},g(e))},onClose:c=>{a(),s(()=>{o(c)},g(t))}}},ag=Symbol("elForwardRef"),vA=e=>{ft(ag,{setForwardRef:n=>{e.value=n}})},mA=e=>({mounted(t){e(t)},updated(t){e(t)},unmounted(){e(null)}}),dp={current:0},pp=V(0),lg=2e3,hp=Symbol("elZIndexContextKey"),ug=Symbol("zIndexContextKey"),Fc=e=>{const t=Ge()?Ee(hp,dp):dp,n=e||(Ge()?Ee(ug,void 0):void 0),r=C(()=>{const i=g(n);return je(i)?i:lg}),o=C(()=>r.value+pp.value),s=()=>(t.current++,pp.value=t.current,o.value);return!st&&Ee(hp),{initialZIndex:r,currentZIndex:o,nextZIndex:s}};function gA(e){let t;function n(){if(e.value==null)return;const{selectionStart:o,selectionEnd:s,value:i}=e.value;if(o==null||s==null)return;const a=i.slice(0,Math.max(0,o)),l=i.slice(Math.max(0,s));t={selectionStart:o,selectionEnd:s,value:i,beforeTxt:a,afterTxt:l}}function r(){if(e.value==null||t==null)return;const{value:o}=e.value,{beforeTxt:s,afterTxt:i,selectionStart:a}=t;if(s==null||i==null||a==null)return;let l=o.length;if(o.endsWith(i))l=o.length-i.length;else if(o.startsWith(s))l=s.length;else{const u=s[a-1],c=o.indexOf(u,a-1);c!==-1&&(l=c+1)}e.value.setSelectionRange(l,l)}return[n,r]}const bA=(e,t,n)=>Qi(e.subTree).filter(s=>{var i;return cn(s)&&((i=s.type)==null?void 0:i.name)===t&&!!s.component}).map(s=>s.component.uid).map(s=>n[s]).filter(s=>!!s),yA=(e,t)=>{const n={},r=ir([]);return{children:r,addChild:i=>{n[i.uid]=i,r.value=bA(e,t,n)},removeChild:i=>{delete n[i],r.value=r.value.filter(a=>a.uid!==i)}}},So=rl({type:String,values:es,required:!1}),cg=Symbol("size"),wA=()=>{const e=Ee(cg,{});return C(()=>g(e.size)||"")};function fg(e,{beforeFocus:t,afterFocus:n,beforeBlur:r,afterBlur:o}={}){const s=Ge(),{emit:i}=s,a=ir(),l=V(!1),u=d=>{ve(t)&&t(d)||l.value||(l.value=!0,i("focus",d),n==null||n())},c=d=>{var v;ve(r)&&r(d)||d.relatedTarget&&((v=a.value)!=null&&v.contains(d.relatedTarget))||(l.value=!1,i("blur",d),o==null||o())},f=()=>{var d,v;(d=a.value)!=null&&d.contains(document.activeElement)&&a.value!==document.activeElement||(v=e.value)==null||v.focus()};return he(a,d=>{d&&d.setAttribute("tabindex","-1")}),tn(a,"focus",u,!0),tn(a,"blur",c,!0),tn(a,"click",f,!0),{isFocused:l,wrapperRef:a,handleFocus:u,handleBlur:c}}function dg({afterComposition:e,emit:t}){const n=V(!1),r=a=>{t==null||t("compositionstart",a),n.value=!0},o=a=>{var l;t==null||t("compositionupdate",a);const u=(l=a.target)==null?void 0:l.value,c=u[u.length-1]||"";n.value=!WT(c)},s=a=>{t==null||t("compositionend",a),n.value&&(n.value=!1,Ie(()=>e(a)))};return{isComposing:n,handleComposition:a=>{a.type==="compositionend"?s(a):o(a)},handleCompositionStart:r,handleCompositionUpdate:o,handleCompositionEnd:s}}const pg=Symbol("emptyValuesContextKey"),SA=["",void 0,null],EA=void 0,hg=xe({emptyValues:Array,valueOnClear:{type:[String,Number,Boolean,Function],default:void 0,validator:e=>ve(e)?!e():!e}}),_A=(e,t)=>{const n=Ge()?Ee(pg,V({})):V({}),r=C(()=>e.emptyValues||n.value.emptyValues||SA),o=C(()=>ve(e.valueOnClear)?e.valueOnClear():e.valueOnClear!==void 0?e.valueOnClear:ve(n.value.valueOnClear)?n.value.valueOnClear():n.value.valueOnClear!==void 0?n.value.valueOnClear:EA),s=i=>r.value.includes(i);return r.value.includes(o.value),{emptyValues:r,valueOnClear:o,isEmptyValue:s}},CA=xe({ariaLabel:String,ariaOrientation:{type:String,values:["horizontal","vertical","undefined"]},ariaControls:String}),yr=e=>Im(CA,e),vg=Symbol(),Ia=V();function ll(e,t=void 0){const n=Ge()?Ee(vg,Ia):Ia;return e?C(()=>{var r,o;return(o=(r=n.value)==null?void 0:r[e])!=null?o:t}):n}function Bc(e,t){const n=ll(),r=$e(e,C(()=>{var a;return((a=n.value)==null?void 0:a.namespace)||Is})),o=sl(C(()=>{var a;return(a=n.value)==null?void 0:a.locale})),s=Fc(C(()=>{var a;return((a=n.value)==null?void 0:a.zIndex)||lg})),i=C(()=>{var a;return g(t)||((a=n.value)==null?void 0:a.size)||""});return TA(C(()=>g(n)||{})),{ns:r,locale:o,zIndex:s,size:i}}const TA=(e,t,n=!1)=>{var r;const o=!!Ge(),s=o?ll():void 0,i=(r=void 0)!=null?r:o?ft:void 0;if(!i)return;const a=C(()=>{const l=g(e);return s!=null&&s.value?OA(s.value,l):l});return i(vg,a),i(Hm,C(()=>a.value.locale)),i(Um,C(()=>a.value.namespace)),i(ug,C(()=>a.value.zIndex)),i(cg,{size:C(()=>a.value.size||"")}),i(pg,C(()=>({emptyValues:a.value.emptyValues,valueOnClear:a.value.valueOnClear}))),(n||!Ia.value)&&(Ia.value=a.value),a},OA=(e,t)=>{const n=[...new Set([...Zd(e),...Zd(t)])],r={};for(const o of n)r[o]=t[o]!==void 0?t[o]:e[o];return r};xe({a11y:{type:Boolean,default:!0},locale:{type:we(Object)},size:So,button:{type:we(Object)},experimentalFeatures:{type:we(Object)},keyboardNavigation:{type:Boolean,default:!0},message:{type:we(Object)},zIndex:Number,namespace:{type:String,default:"el"},...hg});const Fn={};var Ne=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n};const AA=xe({size:{type:we([Number,String])},color:{type:String}}),RA=Z({name:"ElIcon",inheritAttrs:!1}),xA=Z({...RA,props:AA,setup(e){const t=e,n=$e("icon"),r=C(()=>{const{size:o,color:s}=t;return!o&&!s?{}:{fontSize:Lt(o)?void 0:pn(o),"--color":s}});return(o,s)=>(L(),ee("i",En({class:g(n).b(),style:g(r)},o.$attrs),[de(o.$slots,"default")],16))}});var IA=Ne(xA,[["__file","icon.vue"]]);const Je=yt(IA),ts=Symbol("formContextKey"),ho=Symbol("formItemContextKey"),In=(e,t={})=>{const n=V(void 0),r=t.prop?n:qm("size"),o=t.global?n:wA(),s=t.form?{size:void 0}:Ee(ts,void 0),i=t.formItem?{size:void 0}:Ee(ho,void 0);return C(()=>r.value||g(e)||(i==null?void 0:i.size)||(s==null?void 0:s.size)||o.value||"")},ns=e=>{const t=qm("disabled"),n=Ee(ts,void 0);return C(()=>t.value||g(e)||(n==null?void 0:n.disabled)||!1)},Vr=()=>{const e=Ee(ts,void 0),t=Ee(ho,void 0);return{form:e,formItem:t}},hi=(e,{formItemContext:t,disableIdGeneration:n,disableIdManagement:r})=>{n||(n=V(!1)),r||(r=V(!1));const o=V();let s;const i=C(()=>{var a;return!!(!(e.label||e.ariaLabel)&&t&&t.inputIds&&((a=t.inputIds)==null?void 0:a.length)<=1)});return Ke(()=>{s=he([Jt(e,"id"),n],([a,l])=>{const u=a??(l?void 0:pr().value);u!==o.value&&(t!=null&&t.removeInputId&&(o.value&&t.removeInputId(o.value),!(r!=null&&r.value)&&!l&&u&&t.addInputId(u)),o.value=u)},{immediate:!0})}),kr(()=>{s&&s(),t!=null&&t.removeInputId&&o.value&&t.removeInputId(o.value)}),{isLabeledByFormItem:i,inputId:o}},PA=xe({size:{type:String,values:es},disabled:Boolean}),NA=xe({...PA,model:Object,rules:{type:we(Object)},labelPosition:{type:String,values:["left","right","top"],default:"right"},requireAsteriskPosition:{type:String,values:["left","right"],default:"left"},labelWidth:{type:[String,Number],default:""},labelSuffix:{type:String,default:""},inline:Boolean,inlineMessage:Boolean,statusIcon:Boolean,showMessage:{type:Boolean,default:!0},validateOnRuleChange:{type:Boolean,default:!0},hideRequiredAsterisk:Boolean,scrollToError:Boolean,scrollIntoViewOptions:{type:[Object,Boolean]}}),LA={validate:(e,t,n)=>(pe(e)||Ae(e))&&Bt(t)&&Ae(n)};function MA(){const e=V([]),t=C(()=>{if(!e.value.length)return"0";const s=Math.max(...e.value);return s?`${s}px`:""});function n(s){const i=e.value.indexOf(s);return i===-1&&t.value,i}function r(s,i){if(s&&i){const a=n(i);e.value.splice(a,1,s)}else s&&e.value.push(s)}function o(s){const i=n(s);i>-1&&e.value.splice(i,1)}return{autoLabelWidth:t,registerLabelWidth:r,deregisterLabelWidth:o}}const Pi=(e,t)=>{const n=yn(t);return n.length>0?e.filter(r=>r.prop&&n.includes(r.prop)):e},$A="ElForm",kA=Z({name:$A}),FA=Z({...kA,props:NA,emits:LA,setup(e,{expose:t,emit:n}){const r=e,o=[],s=In(),i=$e("form"),a=C(()=>{const{labelPosition:_,inline:w}=r;return[i.b(),i.m(s.value||"default"),{[i.m(`label-${_}`)]:_,[i.m("inline")]:w}]}),l=_=>o.find(w=>w.prop===_),u=_=>{o.push(_)},c=_=>{_.prop&&o.splice(o.indexOf(_),1)},f=(_=[])=>{r.model&&Pi(o,_).forEach(w=>w.resetField())},d=(_=[])=>{Pi(o,_).forEach(w=>w.clearValidate())},v=C(()=>!!r.model),p=_=>{if(o.length===0)return[];const w=Pi(o,_);return w.length?w:[]},h=async _=>m(void 0,_),b=async(_=[])=>{if(!v.value)return!1;const w=p(_);if(w.length===0)return!0;let y={};for(const A of w)try{await A.validate("")}catch(R){y={...y,...R}}return Object.keys(y).length===0?!0:Promise.reject(y)},m=async(_=[],w)=>{const y=!ve(w);try{const A=await b(_);return A===!0&&await(w==null?void 0:w(A)),A}catch(A){if(A instanceof Error)throw A;const R=A;return r.scrollToError&&S(Object.keys(R)[0]),await(w==null?void 0:w(!1,R)),y&&Promise.reject(R)}},S=_=>{var w;const y=Pi(o,_)[0];y&&((w=y.$el)==null||w.scrollIntoView(r.scrollIntoViewOptions))};return he(()=>r.rules,()=>{r.validateOnRuleChange&&h().catch(_=>void 0)},{deep:!0}),ft(ts,wt({...vr(r),emit:n,resetFields:f,clearValidate:d,validateField:m,getField:l,addField:u,removeField:c,...MA()})),t({validate:h,validateField:m,resetFields:f,clearValidate:d,scrollToField:S,fields:o}),(_,w)=>(L(),ee("form",{class:U(g(a))},[de(_.$slots,"default")],2))}});var BA=Ne(FA,[["__file","form.vue"]]);function eo(){return eo=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function ta(e,t,n){return VA()?ta=Reflect.construct.bind():ta=function(o,s,i){var a=[null];a.push.apply(a,s);var l=Function.bind.apply(o,a),u=new l;return i&&Qs(u,i.prototype),u},ta.apply(null,arguments)}function jA(e){return Function.toString.call(e).indexOf("[native code]")!==-1}function xu(e){var t=typeof Map=="function"?new Map:void 0;return xu=function(r){if(r===null||!jA(r))return r;if(typeof r!="function")throw new TypeError("Super expression must either be null or a function");if(typeof t<"u"){if(t.has(r))return t.get(r);t.set(r,o)}function o(){return ta(r,arguments,Ru(this).constructor)}return o.prototype=Object.create(r.prototype,{constructor:{value:o,enumerable:!1,writable:!0,configurable:!0}}),Qs(o,r)},xu(e)}var zA=/%[sdj%]/g,HA=function(){};function Iu(e){if(!e||!e.length)return null;var t={};return e.forEach(function(n){var r=n.field;t[r]=t[r]||[],t[r].push(n)}),t}function Zt(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r=s)return a;switch(a){case"%s":return String(n[o++]);case"%d":return Number(n[o++]);case"%j":try{return JSON.stringify(n[o++])}catch{return"[Circular]"}break;default:return a}});return i}return e}function UA(e){return e==="string"||e==="url"||e==="hex"||e==="email"||e==="date"||e==="pattern"}function bt(e,t){return!!(e==null||t==="array"&&Array.isArray(e)&&!e.length||UA(t)&&typeof e=="string"&&!e)}function KA(e,t,n){var r=[],o=0,s=e.length;function i(a){r.push.apply(r,a||[]),o++,o===s&&n(r)}e.forEach(function(a){t(a,i)})}function vp(e,t,n){var r=0,o=e.length;function s(i){if(i&&i.length){n(i);return}var a=r;r=r+1,a()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,hex:/^#?([a-f0-9]{6}|[a-f0-9]{3})$/i},bs={integer:function(t){return bs.number(t)&&parseInt(t,10)===t},float:function(t){return bs.number(t)&&!bs.integer(t)},array:function(t){return Array.isArray(t)},regexp:function(t){if(t instanceof RegExp)return!0;try{return!!new RegExp(t)}catch{return!1}},date:function(t){return typeof t.getTime=="function"&&typeof t.getMonth=="function"&&typeof t.getYear=="function"&&!isNaN(t.getTime())},number:function(t){return isNaN(t)?!1:typeof t=="number"},object:function(t){return typeof t=="object"&&!bs.array(t)},method:function(t){return typeof t=="function"},email:function(t){return typeof t=="string"&&t.length<=320&&!!t.match(yp.email)},url:function(t){return typeof t=="string"&&t.length<=2048&&!!t.match(XA())},hex:function(t){return typeof t=="string"&&!!t.match(yp.hex)}},ZA=function(t,n,r,o,s){if(t.required&&n===void 0){mg(t,n,r,o,s);return}var i=["integer","float","array","regexp","object","method","email","number","date","url","hex"],a=t.type;i.indexOf(a)>-1?bs[a](n)||o.push(Zt(s.messages.types[a],t.fullField,t.type)):a&&typeof n!==t.type&&o.push(Zt(s.messages.types[a],t.fullField,t.type))},QA=function(t,n,r,o,s){var i=typeof t.len=="number",a=typeof t.min=="number",l=typeof t.max=="number",u=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,c=n,f=null,d=typeof n=="number",v=typeof n=="string",p=Array.isArray(n);if(d?f="number":v?f="string":p&&(f="array"),!f)return!1;p&&(c=n.length),v&&(c=n.replace(u,"_").length),i?c!==t.len&&o.push(Zt(s.messages[f].len,t.fullField,t.len)):a&&!l&&ct.max?o.push(Zt(s.messages[f].max,t.fullField,t.max)):a&&l&&(ct.max)&&o.push(Zt(s.messages[f].range,t.fullField,t.min,t.max))},Co="enum",e4=function(t,n,r,o,s){t[Co]=Array.isArray(t[Co])?t[Co]:[],t[Co].indexOf(n)===-1&&o.push(Zt(s.messages[Co],t.fullField,t[Co].join(", ")))},t4=function(t,n,r,o,s){if(t.pattern){if(t.pattern instanceof RegExp)t.pattern.lastIndex=0,t.pattern.test(n)||o.push(Zt(s.messages.pattern.mismatch,t.fullField,n,t.pattern));else if(typeof t.pattern=="string"){var i=new RegExp(t.pattern);i.test(n)||o.push(Zt(s.messages.pattern.mismatch,t.fullField,n,t.pattern))}}},Be={required:mg,whitespace:JA,type:ZA,range:QA,enum:e4,pattern:t4},n4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n,"string")&&!t.required)return r();Be.required(t,n,o,i,s,"string"),bt(n,"string")||(Be.type(t,n,o,i,s),Be.range(t,n,o,i,s),Be.pattern(t,n,o,i,s),t.whitespace===!0&&Be.whitespace(t,n,o,i,s))}r(i)},r4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&Be.type(t,n,o,i,s)}r(i)},o4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(n===""&&(n=void 0),bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&(Be.type(t,n,o,i,s),Be.range(t,n,o,i,s))}r(i)},s4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&Be.type(t,n,o,i,s)}r(i)},i4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),bt(n)||Be.type(t,n,o,i,s)}r(i)},a4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&(Be.type(t,n,o,i,s),Be.range(t,n,o,i,s))}r(i)},l4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&(Be.type(t,n,o,i,s),Be.range(t,n,o,i,s))}r(i)},u4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(n==null&&!t.required)return r();Be.required(t,n,o,i,s,"array"),n!=null&&(Be.type(t,n,o,i,s),Be.range(t,n,o,i,s))}r(i)},c4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&Be.type(t,n,o,i,s)}r(i)},f4="enum",d4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s),n!==void 0&&Be[f4](t,n,o,i,s)}r(i)},p4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n,"string")&&!t.required)return r();Be.required(t,n,o,i,s),bt(n,"string")||Be.pattern(t,n,o,i,s)}r(i)},h4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n,"date")&&!t.required)return r();if(Be.required(t,n,o,i,s),!bt(n,"date")){var l;n instanceof Date?l=n:l=new Date(n),Be.type(t,l,o,i,s),l&&Be.range(t,l.getTime(),o,i,s)}}r(i)},v4=function(t,n,r,o,s){var i=[],a=Array.isArray(n)?"array":typeof n;Be.required(t,n,o,i,s,a),r(i)},Bl=function(t,n,r,o,s){var i=t.type,a=[],l=t.required||!t.required&&o.hasOwnProperty(t.field);if(l){if(bt(n,i)&&!t.required)return r();Be.required(t,n,o,a,s,i),bt(n,i)||Be.type(t,n,o,a,s)}r(a)},m4=function(t,n,r,o,s){var i=[],a=t.required||!t.required&&o.hasOwnProperty(t.field);if(a){if(bt(n)&&!t.required)return r();Be.required(t,n,o,i,s)}r(i)},Ls={string:n4,method:r4,number:o4,boolean:s4,regexp:i4,integer:a4,float:l4,array:u4,object:c4,enum:d4,pattern:p4,date:h4,url:Bl,hex:Bl,email:Bl,required:v4,any:m4};function Pu(){return{default:"Validation error on field %s",required:"%s is required",enum:"%s must be one of %s",whitespace:"%s cannot be empty",date:{format:"%s date %s is invalid for format %s",parse:"%s date could not be parsed, %s is invalid ",invalid:"%s date %s is invalid"},types:{string:"%s is not a %s",method:"%s is not a %s (function)",array:"%s is not an %s",object:"%s is not an %s",number:"%s is not a %s",date:"%s is not a %s",boolean:"%s is not a %s",integer:"%s is not an %s",float:"%s is not a %s",regexp:"%s is not a valid %s",email:"%s is not a valid %s",url:"%s is not a valid %s",hex:"%s is not a valid %s"},string:{len:"%s must be exactly %s characters",min:"%s must be at least %s characters",max:"%s cannot be longer than %s characters",range:"%s must be between %s and %s characters"},number:{len:"%s must equal %s",min:"%s cannot be less than %s",max:"%s cannot be greater than %s",range:"%s must be between %s and %s"},array:{len:"%s must be exactly %s in length",min:"%s cannot be less than %s in length",max:"%s cannot be greater than %s in length",range:"%s must be between %s and %s in length"},pattern:{mismatch:"%s value %s does not match pattern %s"},clone:function(){var t=JSON.parse(JSON.stringify(this));return t.clone=this.clone,t}}}var Nu=Pu(),vi=function(){function e(n){this.rules=null,this._messages=Nu,this.define(n)}var t=e.prototype;return t.define=function(r){var o=this;if(!r)throw new Error("Cannot configure a schema with no rules");if(typeof r!="object"||Array.isArray(r))throw new Error("Rules must be an object");this.rules={},Object.keys(r).forEach(function(s){var i=r[s];o.rules[s]=Array.isArray(i)?i:[i]})},t.messages=function(r){return r&&(this._messages=bp(Pu(),r)),this._messages},t.validate=function(r,o,s){var i=this;o===void 0&&(o={}),s===void 0&&(s=function(){});var a=r,l=o,u=s;if(typeof l=="function"&&(u=l,l={}),!this.rules||Object.keys(this.rules).length===0)return u&&u(null,a),Promise.resolve(a);function c(h){var b=[],m={};function S(w){if(Array.isArray(w)){var y;b=(y=b).concat.apply(y,w)}else b.push(w)}for(var _=0;_");const o=$e("form"),s=V(),i=V(0),a=()=>{var c;if((c=s.value)!=null&&c.firstElementChild){const f=window.getComputedStyle(s.value.firstElementChild).width;return Math.ceil(Number.parseFloat(f))}else return 0},l=(c="update")=>{Ie(()=>{t.default&&e.isAutoWidth&&(c==="update"?i.value=a():c==="remove"&&(n==null||n.deregisterLabelWidth(i.value)))})},u=()=>l("update");return Ke(()=>{u()}),St(()=>{l("remove")}),mo(()=>u()),he(i,(c,f)=>{e.updateAll&&(n==null||n.registerLabelWidth(c,f))}),Dt(C(()=>{var c,f;return(f=(c=s.value)==null?void 0:c.firstElementChild)!=null?f:null}),u),()=>{var c,f;if(!t)return null;const{isAutoWidth:d}=e;if(d){const v=n==null?void 0:n.autoLabelWidth,p=r==null?void 0:r.hasLabel,h={};if(p&&v&&v!=="auto"){const b=Math.max(0,Number.parseInt(v,10)-i.value),S=(r.labelPosition||n.labelPosition)==="left"?"marginRight":"marginLeft";b&&(h[S]=`${b}px`)}return re("div",{ref:s,class:[o.be("item","label-wrap")],style:h},[(c=t.default)==null?void 0:c.call(t)])}else return re(nt,{ref:s},[(f=t.default)==null?void 0:f.call(t)])}}});const w4=Z({name:"ElFormItem"}),S4=Z({...w4,props:b4,setup(e,{expose:t}){const n=e,r=go(),o=Ee(ts,void 0),s=Ee(ho,void 0),i=In(void 0,{formItem:!1}),a=$e("form-item"),l=pr().value,u=V([]),c=V(""),f=V1(c,100),d=V(""),v=V();let p,h=!1;const b=C(()=>n.labelPosition||(o==null?void 0:o.labelPosition)),m=C(()=>{if(b.value==="top")return{};const K=pn(n.labelWidth||(o==null?void 0:o.labelWidth)||"");return K?{width:K}:{}}),S=C(()=>{if(b.value==="top"||o!=null&&o.inline)return{};if(!n.label&&!n.labelWidth&&x)return{};const K=pn(n.labelWidth||(o==null?void 0:o.labelWidth)||"");return!n.label&&!r.label?{marginLeft:K}:{}}),_=C(()=>[a.b(),a.m(i.value),a.is("error",c.value==="error"),a.is("validating",c.value==="validating"),a.is("success",c.value==="success"),a.is("required",P.value||n.required),a.is("no-asterisk",o==null?void 0:o.hideRequiredAsterisk),(o==null?void 0:o.requireAsteriskPosition)==="right"?"asterisk-right":"asterisk-left",{[a.m("feedback")]:o==null?void 0:o.statusIcon,[a.m(`label-${b.value}`)]:b.value}]),w=C(()=>Bt(n.inlineMessage)?n.inlineMessage:(o==null?void 0:o.inlineMessage)||!1),y=C(()=>[a.e("error"),{[a.em("error","inline")]:w.value}]),A=C(()=>n.prop?Ae(n.prop)?n.prop:n.prop.join("."):""),R=C(()=>!!(n.label||r.label)),N=C(()=>n.for||(u.value.length===1?u.value[0]:void 0)),I=C(()=>!N.value&&R.value),x=!!s,k=C(()=>{const K=o==null?void 0:o.model;if(!(!K||!n.prop))return Fl(K,n.prop).value}),$=C(()=>{const{required:K}=n,J=[];n.rules&&J.push(...yn(n.rules));const oe=o==null?void 0:o.rules;if(oe&&n.prop){const ge=Fl(oe,n.prop).value;ge&&J.push(...yn(ge))}if(K!==void 0){const ge=J.map((E,T)=>[E,T]).filter(([E])=>Object.keys(E).includes("required"));if(ge.length>0)for(const[E,T]of ge)E.required!==K&&(J[T]={...E,required:K});else J.push({required:K})}return J}),F=C(()=>$.value.length>0),Y=K=>$.value.filter(oe=>!oe.trigger||!K?!0:Array.isArray(oe.trigger)?oe.trigger.includes(K):oe.trigger===K).map(({trigger:oe,...ge})=>ge),P=C(()=>$.value.some(K=>K.required)),O=C(()=>{var K;return f.value==="error"&&n.showMessage&&((K=o==null?void 0:o.showMessage)!=null?K:!0)}),j=C(()=>`${n.label||""}${(o==null?void 0:o.labelSuffix)||""}`),Q=K=>{c.value=K},me=K=>{var J,oe;const{errors:ge,fields:E}=K;Q("error"),d.value=ge?(oe=(J=ge==null?void 0:ge[0])==null?void 0:J.message)!=null?oe:`${n.prop} is required`:"",o==null||o.emit("validate",n.prop,!1,d.value)},Pe=()=>{Q("success"),o==null||o.emit("validate",n.prop,!0,"")},Re=async K=>{const J=A.value;return new vi({[J]:K}).validate({[J]:k.value},{firstFields:!0}).then(()=>(Pe(),!0)).catch(ge=>(me(ge),Promise.reject(ge)))},Ce=async(K,J)=>{if(h||!n.prop)return!1;const oe=ve(J);if(!F.value)return J==null||J(!1),!1;const ge=Y(K);return ge.length===0?(J==null||J(!0),!0):(Q("validating"),Re(ge).then(()=>(J==null||J(!0),!0)).catch(E=>{const{fields:T}=E;return J==null||J(!1,T),oe?!1:Promise.reject(T)}))},_e=()=>{Q(""),d.value="",h=!1},qe=async()=>{const K=o==null?void 0:o.model;if(!K||!n.prop)return;const J=Fl(K,n.prop);h=!0,J.value=Wd(p),await Ie(),_e(),h=!1},ze=K=>{u.value.includes(K)||u.value.push(K)},De=K=>{u.value=u.value.filter(J=>J!==K)};he(()=>n.error,K=>{d.value=K||"",Q(K?"error":"")},{immediate:!0}),he(()=>n.validateStatus,K=>Q(K||""));const B=wt({...vr(n),$el:v,size:i,validateState:c,labelId:l,inputIds:u,isGroup:I,hasLabel:R,fieldValue:k,addInputId:ze,removeInputId:De,resetField:qe,clearValidate:_e,validate:Ce});return ft(ho,B),Ke(()=>{n.prop&&(o==null||o.addField(B),p=Wd(k.value))}),St(()=>{o==null||o.removeField(B)}),t({size:i,validateMessage:d,validateState:c,validate:Ce,clearValidate:_e,resetField:qe}),(K,J)=>{var oe;return L(),ee("div",{ref_key:"formItemRef",ref:v,class:U(g(_)),role:g(I)?"group":void 0,"aria-labelledby":g(I)?g(l):void 0},[re(g(y4),{"is-auto-width":g(m).width==="auto","update-all":((oe=g(o))==null?void 0:oe.labelWidth)==="auto"},{default:ce(()=>[g(R)?(L(),fe(Xe(g(N)?"label":"div"),{key:0,id:g(l),for:g(N),class:U(g(a).e("label")),style:ot(g(m))},{default:ce(()=>[de(K.$slots,"label",{label:g(j)},()=>[Un(He(g(j)),1)])]),_:3},8,["id","for","class","style"])):ae("v-if",!0)]),_:3},8,["is-auto-width","update-all"]),le("div",{class:U(g(a).e("content")),style:ot(g(S))},[de(K.$slots,"default"),re(sw,{name:`${g(a).namespace.value}-zoom-in-top`},{default:ce(()=>[g(O)?de(K.$slots,"error",{key:0,error:d.value},()=>[le("div",{class:U(g(y))},He(d.value),3)]):ae("v-if",!0)]),_:3},8,["name"])],6)],10,["role","aria-labelledby"])}}});var gg=Ne(S4,[["__file","form-item.vue"]]);const IN=yt(BA,{FormItem:gg}),PN=wo(gg);let mn;const E4=` + height:0 !important; + visibility:hidden !important; + ${oS()?"":"overflow:hidden !important;"} + position:absolute !important; + z-index:-1000 !important; + top:0 !important; + right:0 !important; +`,_4=["letter-spacing","line-height","padding-top","padding-bottom","font-family","font-weight","font-size","text-rendering","text-transform","width","text-indent","padding-left","padding-right","border-width","box-sizing"];function C4(e){const t=window.getComputedStyle(e),n=t.getPropertyValue("box-sizing"),r=Number.parseFloat(t.getPropertyValue("padding-bottom"))+Number.parseFloat(t.getPropertyValue("padding-top")),o=Number.parseFloat(t.getPropertyValue("border-bottom-width"))+Number.parseFloat(t.getPropertyValue("border-top-width"));return{contextStyle:_4.map(i=>`${i}:${t.getPropertyValue(i)}`).join(";"),paddingSize:r,borderSize:o,boxSizing:n}}function Sp(e,t=1,n){var r;mn||(mn=document.createElement("textarea"),document.body.appendChild(mn));const{paddingSize:o,borderSize:s,boxSizing:i,contextStyle:a}=C4(e);mn.setAttribute("style",`${a};${E4}`),mn.value=e.value||e.placeholder||"";let l=mn.scrollHeight;const u={};i==="border-box"?l=l+s:i==="content-box"&&(l=l-o),mn.value="";const c=mn.scrollHeight-o;if(je(t)){let f=c*t;i==="border-box"&&(f=f+o+s),l=Math.max(f,l),u.minHeight=`${f}px`}if(je(n)){let f=c*n;i==="border-box"&&(f=f+o+s),l=Math.min(f,l)}return u.height=`${l}px`,(r=mn.parentNode)==null||r.removeChild(mn),mn=void 0,u}const T4=xe({id:{type:String,default:void 0},size:So,disabled:Boolean,modelValue:{type:we([String,Number,Object]),default:""},maxlength:{type:[String,Number]},minlength:{type:[String,Number]},type:{type:String,default:"text"},resize:{type:String,values:["none","both","horizontal","vertical"]},autosize:{type:we([Boolean,Object]),default:!1},autocomplete:{type:String,default:"off"},formatter:{type:Function},parser:{type:Function},placeholder:{type:String},form:{type:String},readonly:Boolean,clearable:Boolean,showPassword:Boolean,showWordLimit:Boolean,suffixIcon:{type:jt},prefixIcon:{type:jt},containerRole:{type:String,default:void 0},tabindex:{type:[String,Number],default:0},validateEvent:{type:Boolean,default:!0},inputStyle:{type:we([Object,Array,String]),default:()=>ol({})},autofocus:Boolean,rows:{type:Number,default:2},...yr(["ariaLabel"])}),O4={[rt]:e=>Ae(e),input:e=>Ae(e),change:e=>Ae(e),focus:e=>e instanceof FocusEvent,blur:e=>e instanceof FocusEvent,clear:()=>!0,mouseleave:e=>e instanceof MouseEvent,mouseenter:e=>e instanceof MouseEvent,keydown:e=>e instanceof Event,compositionstart:e=>e instanceof CompositionEvent,compositionupdate:e=>e instanceof CompositionEvent,compositionend:e=>e instanceof CompositionEvent},A4=Z({name:"ElInput",inheritAttrs:!1}),R4=Z({...A4,props:T4,emits:O4,setup(e,{expose:t,emit:n}){const r=e,o=Zy(),s=go(),i=C(()=>{const z={};return r.containerRole==="combobox"&&(z["aria-haspopup"]=o["aria-haspopup"],z["aria-owns"]=o["aria-owns"],z["aria-expanded"]=o["aria-expanded"]),z}),a=C(()=>[r.type==="textarea"?b.b():h.b(),h.m(v.value),h.is("disabled",p.value),h.is("exceed",_e.value),{[h.b("group")]:s.prepend||s.append,[h.m("prefix")]:s.prefix||r.prefixIcon,[h.m("suffix")]:s.suffix||r.suffixIcon||r.clearable||r.showPassword,[h.bm("suffix","password-clear")]:me.value&&Pe.value,[h.b("hidden")]:r.type==="hidden"},o.class]),l=C(()=>[h.e("wrapper"),h.is("focus",I.value)]),u=JT({excludeKeys:C(()=>Object.keys(i.value))}),{form:c,formItem:f}=Vr(),{inputId:d}=hi(r,{formItemContext:f}),v=In(),p=ns(),h=$e("input"),b=$e("textarea"),m=ir(),S=ir(),_=V(!1),w=V(!1),y=V(),A=ir(r.inputStyle),R=C(()=>m.value||S.value),{wrapperRef:N,isFocused:I,handleFocus:x,handleBlur:k}=fg(R,{beforeFocus(){return p.value},afterBlur(){var z;r.validateEvent&&((z=f==null?void 0:f.validate)==null||z.call(f,"blur").catch(be=>void 0))}}),$=C(()=>{var z;return(z=c==null?void 0:c.statusIcon)!=null?z:!1}),F=C(()=>(f==null?void 0:f.validateState)||""),Y=C(()=>F.value&&Vm[F.value]),P=C(()=>w.value?VT:IT),O=C(()=>[o.style]),j=C(()=>[r.inputStyle,A.value,{resize:r.resize}]),Q=C(()=>qn(r.modelValue)?"":String(r.modelValue)),me=C(()=>r.clearable&&!p.value&&!r.readonly&&!!Q.value&&(I.value||_.value)),Pe=C(()=>r.showPassword&&!p.value&&!r.readonly&&!!Q.value&&(!!Q.value||I.value)),Re=C(()=>r.showWordLimit&&!!r.maxlength&&(r.type==="text"||r.type==="textarea")&&!p.value&&!r.readonly&&!r.showPassword),Ce=C(()=>Q.value.length),_e=C(()=>!!Re.value&&Ce.value>Number(r.maxlength)),qe=C(()=>!!s.suffix||!!r.suffixIcon||me.value||r.showPassword||Re.value||!!F.value&&$.value),[ze,De]=gA(m);Dt(S,z=>{if(J(),!Re.value||r.resize!=="both")return;const be=z[0],{width:Le}=be.contentRect;y.value={right:`calc(100% - ${Le+15+6}px)`}});const B=()=>{const{type:z,autosize:be}=r;if(!(!st||z!=="textarea"||!S.value))if(be){const Le=Oe(be)?be.minRows:void 0,ke=Oe(be)?be.maxRows:void 0,dt=Sp(S.value,Le,ke);A.value={overflowY:"hidden",...dt},Ie(()=>{S.value.offsetHeight,A.value=dt})}else A.value={minHeight:Sp(S.value).minHeight}},J=(z=>{let be=!1;return()=>{var Le;if(be||!r.autosize)return;((Le=S.value)==null?void 0:Le.offsetParent)===null||(z(),be=!0)}})(B),oe=()=>{const z=R.value,be=r.formatter?r.formatter(Q.value):Q.value;!z||z.value===be||(z.value=be)},ge=async z=>{ze();let{value:be}=z.target;if(r.formatter&&(be=r.parser?r.parser(be):be),!T.value){if(be===Q.value){oe();return}n(rt,be),n("input",be),await Ie(),oe(),De()}},E=z=>{n("change",z.target.value)},{isComposing:T,handleCompositionStart:M,handleCompositionUpdate:W,handleCompositionEnd:G}=dg({emit:n,afterComposition:ge}),q=()=>{w.value=!w.value,se()},se=async()=>{var z;await Ie(),(z=R.value)==null||z.focus()},ne=()=>{var z;return(z=R.value)==null?void 0:z.blur()},te=z=>{_.value=!1,n("mouseleave",z)},X=z=>{_.value=!0,n("mouseenter",z)},Se=z=>{n("keydown",z)},ue=()=>{var z;(z=R.value)==null||z.select()},ye=()=>{n(rt,""),n("change",""),n("clear"),n("input","")};return he(()=>r.modelValue,()=>{var z;Ie(()=>B()),r.validateEvent&&((z=f==null?void 0:f.validate)==null||z.call(f,"change").catch(be=>void 0))}),he(Q,()=>oe()),he(()=>r.type,async()=>{await Ie(),oe(),B()}),Ke(()=>{!r.formatter&&r.parser,oe(),Ie(B)}),t({input:m,textarea:S,ref:R,textareaStyle:j,autosize:Jt(r,"autosize"),isComposing:T,focus:se,blur:ne,select:ue,clear:ye,resizeTextarea:B}),(z,be)=>(L(),ee("div",En(g(i),{class:[g(a),{[g(h).bm("group","append")]:z.$slots.append,[g(h).bm("group","prepend")]:z.$slots.prepend}],style:g(O),role:z.containerRole,onMouseenter:X,onMouseleave:te}),[ae(" input "),z.type!=="textarea"?(L(),ee(nt,{key:0},[ae(" prepend slot "),z.$slots.prepend?(L(),ee("div",{key:0,class:U(g(h).be("group","prepend"))},[de(z.$slots,"prepend")],2)):ae("v-if",!0),le("div",{ref_key:"wrapperRef",ref:N,class:U(g(l))},[ae(" prefix slot "),z.$slots.prefix||z.prefixIcon?(L(),ee("span",{key:0,class:U(g(h).e("prefix"))},[le("span",{class:U(g(h).e("prefix-inner"))},[de(z.$slots,"prefix"),z.prefixIcon?(L(),fe(g(Je),{key:0,class:U(g(h).e("icon"))},{default:ce(()=>[(L(),fe(Xe(z.prefixIcon)))]),_:1},8,["class"])):ae("v-if",!0)],2)],2)):ae("v-if",!0),le("input",En({id:g(d),ref_key:"input",ref:m,class:g(h).e("inner")},g(u),{minlength:z.minlength,maxlength:z.maxlength,type:z.showPassword?w.value?"text":"password":z.type,disabled:g(p),readonly:z.readonly,autocomplete:z.autocomplete,tabindex:z.tabindex,"aria-label":z.ariaLabel,placeholder:z.placeholder,style:z.inputStyle,form:z.form,autofocus:z.autofocus,onCompositionstart:g(M),onCompositionupdate:g(W),onCompositionend:g(G),onInput:ge,onChange:E,onKeydown:Se}),null,16,["id","minlength","maxlength","type","disabled","readonly","autocomplete","tabindex","aria-label","placeholder","form","autofocus","onCompositionstart","onCompositionupdate","onCompositionend"]),ae(" suffix slot "),g(qe)?(L(),ee("span",{key:1,class:U(g(h).e("suffix"))},[le("span",{class:U(g(h).e("suffix-inner"))},[!g(me)||!g(Pe)||!g(Re)?(L(),ee(nt,{key:0},[de(z.$slots,"suffix"),z.suffixIcon?(L(),fe(g(Je),{key:0,class:U(g(h).e("icon"))},{default:ce(()=>[(L(),fe(Xe(z.suffixIcon)))]),_:1},8,["class"])):ae("v-if",!0)],64)):ae("v-if",!0),g(me)?(L(),fe(g(Je),{key:1,class:U([g(h).e("icon"),g(h).e("clear")]),onMousedown:tt(g(ht),["prevent"]),onClick:ye},{default:ce(()=>[re(g(Oc))]),_:1},8,["class","onMousedown"])):ae("v-if",!0),g(Pe)?(L(),fe(g(Je),{key:2,class:U([g(h).e("icon"),g(h).e("password")]),onClick:q},{default:ce(()=>[(L(),fe(Xe(g(P))))]),_:1},8,["class"])):ae("v-if",!0),g(Re)?(L(),ee("span",{key:3,class:U(g(h).e("count"))},[le("span",{class:U(g(h).e("count-inner"))},He(g(Ce))+" / "+He(z.maxlength),3)],2)):ae("v-if",!0),g(F)&&g(Y)&&g($)?(L(),fe(g(Je),{key:4,class:U([g(h).e("icon"),g(h).e("validateIcon"),g(h).is("loading",g(F)==="validating")])},{default:ce(()=>[(L(),fe(Xe(g(Y))))]),_:1},8,["class"])):ae("v-if",!0)],2)],2)):ae("v-if",!0)],2),ae(" append slot "),z.$slots.append?(L(),ee("div",{key:1,class:U(g(h).be("group","append"))},[de(z.$slots,"append")],2)):ae("v-if",!0)],64)):(L(),ee(nt,{key:1},[ae(" textarea "),le("textarea",En({id:g(d),ref_key:"textarea",ref:S,class:[g(b).e("inner"),g(h).is("focus",g(I))]},g(u),{minlength:z.minlength,maxlength:z.maxlength,tabindex:z.tabindex,disabled:g(p),readonly:z.readonly,autocomplete:z.autocomplete,style:g(j),"aria-label":z.ariaLabel,placeholder:z.placeholder,form:z.form,autofocus:z.autofocus,rows:z.rows,onCompositionstart:g(M),onCompositionupdate:g(W),onCompositionend:g(G),onInput:ge,onFocus:g(x),onBlur:g(k),onChange:E,onKeydown:Se}),null,16,["id","minlength","maxlength","tabindex","disabled","readonly","autocomplete","aria-label","placeholder","form","autofocus","rows","onCompositionstart","onCompositionupdate","onCompositionend","onFocus","onBlur"]),g(Re)?(L(),ee("span",{key:0,style:ot(y.value),class:U(g(h).e("count"))},He(g(Ce))+" / "+He(z.maxlength),7)):ae("v-if",!0)],64))],16,["role"]))}});var x4=Ne(R4,[["__file","input.vue"]]);const bg=yt(x4),To=4,I4={vertical:{offset:"offsetHeight",scroll:"scrollTop",scrollSize:"scrollHeight",size:"height",key:"vertical",axis:"Y",client:"clientY",direction:"top"},horizontal:{offset:"offsetWidth",scroll:"scrollLeft",scrollSize:"scrollWidth",size:"width",key:"horizontal",axis:"X",client:"clientX",direction:"left"}},P4=({move:e,size:t,bar:n})=>({[n.size]:t,transform:`translate${n.axis}(${e}%)`}),Dc=Symbol("scrollbarContextKey"),N4=xe({vertical:Boolean,size:String,move:Number,ratio:{type:Number,required:!0},always:Boolean}),L4="Thumb",M4=Z({__name:"thumb",props:N4,setup(e){const t=e,n=Ee(Dc),r=$e("scrollbar");n||Br(L4,"can not inject scrollbar context");const o=V(),s=V(),i=V({}),a=V(!1);let l=!1,u=!1,c=st?document.onselectstart:null;const f=C(()=>I4[t.vertical?"vertical":"horizontal"]),d=C(()=>P4({size:t.size,move:t.move,bar:f.value})),v=C(()=>o.value[f.value.offset]**2/n.wrapElement[f.value.scrollSize]/t.ratio/s.value[f.value.offset]),p=A=>{var R;if(A.stopPropagation(),A.ctrlKey||[1,2].includes(A.button))return;(R=window.getSelection())==null||R.removeAllRanges(),b(A);const N=A.currentTarget;N&&(i.value[f.value.axis]=N[f.value.offset]-(A[f.value.client]-N.getBoundingClientRect()[f.value.direction]))},h=A=>{if(!s.value||!o.value||!n.wrapElement)return;const R=Math.abs(A.target.getBoundingClientRect()[f.value.direction]-A[f.value.client]),N=s.value[f.value.offset]/2,I=(R-N)*100*v.value/o.value[f.value.offset];n.wrapElement[f.value.scroll]=I*n.wrapElement[f.value.scrollSize]/100},b=A=>{A.stopImmediatePropagation(),l=!0,document.addEventListener("mousemove",m),document.addEventListener("mouseup",S),c=document.onselectstart,document.onselectstart=()=>!1},m=A=>{if(!o.value||!s.value||l===!1)return;const R=i.value[f.value.axis];if(!R)return;const N=(o.value.getBoundingClientRect()[f.value.direction]-A[f.value.client])*-1,I=s.value[f.value.offset]-R,x=(N-I)*100*v.value/o.value[f.value.offset];n.wrapElement[f.value.scroll]=x*n.wrapElement[f.value.scrollSize]/100},S=()=>{l=!1,i.value[f.value.axis]=0,document.removeEventListener("mousemove",m),document.removeEventListener("mouseup",S),y(),u&&(a.value=!1)},_=()=>{u=!1,a.value=!!t.size},w=()=>{u=!0,a.value=l};St(()=>{y(),document.removeEventListener("mouseup",S)});const y=()=>{document.onselectstart!==c&&(document.onselectstart=c)};return tn(Jt(n,"scrollbarElement"),"mousemove",_),tn(Jt(n,"scrollbarElement"),"mouseleave",w),(A,R)=>(L(),fe(Fr,{name:g(r).b("fade"),persisted:""},{default:ce(()=>[ct(le("div",{ref_key:"instance",ref:o,class:U([g(r).e("bar"),g(r).is(g(f).key)]),onMousedown:h},[le("div",{ref_key:"thumb",ref:s,class:U(g(r).e("thumb")),style:ot(g(d)),onMousedown:p},null,38)],34),[[en,A.always||a.value]])]),_:1},8,["name"]))}});var Ep=Ne(M4,[["__file","thumb.vue"]]);const $4=xe({always:{type:Boolean,default:!0},minSize:{type:Number,required:!0}}),k4=Z({__name:"bar",props:$4,setup(e,{expose:t}){const n=e,r=Ee(Dc),o=V(0),s=V(0),i=V(""),a=V(""),l=V(1),u=V(1);return t({handleScroll:d=>{if(d){const v=d.offsetHeight-To,p=d.offsetWidth-To;s.value=d.scrollTop*100/v*l.value,o.value=d.scrollLeft*100/p*u.value}},update:()=>{const d=r==null?void 0:r.wrapElement;if(!d)return;const v=d.offsetHeight-To,p=d.offsetWidth-To,h=v**2/d.scrollHeight,b=p**2/d.scrollWidth,m=Math.max(h,n.minSize),S=Math.max(b,n.minSize);l.value=h/(v-h)/(m/(v-m)),u.value=b/(p-b)/(S/(p-S)),a.value=m+To(L(),ee(nt,null,[re(Ep,{move:o.value,ratio:u.value,size:i.value,always:d.always},null,8,["move","ratio","size","always"]),re(Ep,{move:s.value,ratio:l.value,size:a.value,vertical:"",always:d.always},null,8,["move","ratio","size","always"])],64))}});var F4=Ne(k4,[["__file","bar.vue"]]);const B4=xe({height:{type:[String,Number],default:""},maxHeight:{type:[String,Number],default:""},native:{type:Boolean,default:!1},wrapStyle:{type:we([String,Object,Array]),default:""},wrapClass:{type:[String,Array],default:""},viewClass:{type:[String,Array],default:""},viewStyle:{type:[String,Array,Object],default:""},noresize:Boolean,tag:{type:String,default:"div"},always:Boolean,minSize:{type:Number,default:20},tabindex:{type:[String,Number],default:void 0},id:String,role:String,...yr(["ariaLabel","ariaOrientation"])}),D4={scroll:({scrollTop:e,scrollLeft:t})=>[e,t].every(je)},V4="ElScrollbar",j4=Z({name:V4}),z4=Z({...j4,props:B4,emits:D4,setup(e,{expose:t,emit:n}){const r=e,o=$e("scrollbar");let s,i,a=0,l=0;const u=V(),c=V(),f=V(),d=V(),v=C(()=>{const y={};return r.height&&(y.height=pn(r.height)),r.maxHeight&&(y.maxHeight=pn(r.maxHeight)),[r.wrapStyle,y]}),p=C(()=>[r.wrapClass,o.e("wrap"),{[o.em("wrap","hidden-default")]:!r.native}]),h=C(()=>[o.e("view"),r.viewClass]),b=()=>{var y;c.value&&((y=d.value)==null||y.handleScroll(c.value),a=c.value.scrollTop,l=c.value.scrollLeft,n("scroll",{scrollTop:c.value.scrollTop,scrollLeft:c.value.scrollLeft}))};function m(y,A){Oe(y)?c.value.scrollTo(y):je(y)&&je(A)&&c.value.scrollTo(y,A)}const S=y=>{je(y)&&(c.value.scrollTop=y)},_=y=>{je(y)&&(c.value.scrollLeft=y)},w=()=>{var y;(y=d.value)==null||y.update()};return he(()=>r.noresize,y=>{y?(s==null||s(),i==null||i()):({stop:s}=Dt(f,w),i=tn("resize",w))},{immediate:!0}),he(()=>[r.maxHeight,r.height],()=>{r.native||Ie(()=>{var y;w(),c.value&&((y=d.value)==null||y.handleScroll(c.value))})}),ft(Dc,wt({scrollbarElement:u,wrapElement:c})),Ka(()=>{c.value&&(c.value.scrollTop=a,c.value.scrollLeft=l)}),Ke(()=>{r.native||Ie(()=>{w()})}),mo(()=>w()),t({wrapRef:c,update:w,scrollTo:m,setScrollTop:S,setScrollLeft:_,handleScroll:b}),(y,A)=>(L(),ee("div",{ref_key:"scrollbarRef",ref:u,class:U(g(o).b())},[le("div",{ref_key:"wrapRef",ref:c,class:U(g(p)),style:ot(g(v)),tabindex:y.tabindex,onScroll:b},[(L(),fe(Xe(y.tag),{id:y.id,ref_key:"resizeRef",ref:f,class:U(g(h)),style:ot(y.viewStyle),role:y.role,"aria-label":y.ariaLabel,"aria-orientation":y.ariaOrientation},{default:ce(()=>[de(y.$slots,"default")]),_:3},8,["id","class","style","role","aria-label","aria-orientation"]))],46,["tabindex"]),y.native?ae("v-if",!0):(L(),fe(F4,{key:0,ref_key:"barRef",ref:d,always:y.always,"min-size":y.minSize},null,8,["always","min-size"]))],2))}});var H4=Ne(z4,[["__file","scrollbar.vue"]]);const U4=yt(H4),Vc=Symbol("popper"),yg=Symbol("popperContent"),K4=["dialog","grid","group","listbox","menu","navigation","tooltip","tree"],wg=xe({role:{type:String,values:K4,default:"tooltip"}}),q4=Z({name:"ElPopper",inheritAttrs:!1}),W4=Z({...q4,props:wg,setup(e,{expose:t}){const n=e,r=V(),o=V(),s=V(),i=V(),a=C(()=>n.role),l={triggerRef:r,popperInstanceRef:o,contentRef:s,referenceRef:i,role:a};return t(l),ft(Vc,l),(u,c)=>de(u.$slots,"default")}});var G4=Ne(W4,[["__file","popper.vue"]]);const Sg=xe({arrowOffset:{type:Number,default:5}}),Y4=Z({name:"ElPopperArrow",inheritAttrs:!1}),J4=Z({...Y4,props:Sg,setup(e,{expose:t}){const n=e,r=$e("popper"),{arrowOffset:o,arrowRef:s,arrowStyle:i}=Ee(yg,void 0);return he(()=>n.arrowOffset,a=>{o.value=a}),St(()=>{s.value=void 0}),t({arrowRef:s}),(a,l)=>(L(),ee("span",{ref_key:"arrowRef",ref:s,class:U(g(r).e("arrow")),style:ot(g(i)),"data-popper-arrow":""},null,6))}});var X4=Ne(J4,[["__file","arrow.vue"]]);const Z4="ElOnlyChild",Q4=Z({name:Z4,setup(e,{slots:t,attrs:n}){var r;const o=Ee(ag),s=mA((r=o==null?void 0:o.setForwardRef)!=null?r:ht);return()=>{var i;const a=(i=t.default)==null?void 0:i.call(t,n);if(!a||a.length>1)return null;const l=Eg(a);return l?ct(fr(l,n),[[s]]):null}}});function Eg(e){if(!e)return null;const t=e;for(const n of t){if(Oe(n))switch(n.type){case _t:continue;case Zo:case"svg":return _p(n);case nt:return Eg(n.children);default:return n}return _p(n)}return null}function _p(e){const t=$e("only-child");return re("span",{class:t.e("content")},[e])}const _g=xe({virtualRef:{type:we(Object)},virtualTriggering:Boolean,onMouseenter:{type:we(Function)},onMouseleave:{type:we(Function)},onClick:{type:we(Function)},onKeydown:{type:we(Function)},onFocus:{type:we(Function)},onBlur:{type:we(Function)},onContextmenu:{type:we(Function)},id:String,open:Boolean}),eR=Z({name:"ElPopperTrigger",inheritAttrs:!1}),tR=Z({...eR,props:_g,setup(e,{expose:t}){const n=e,{role:r,triggerRef:o}=Ee(Vc,void 0);vA(o);const s=C(()=>a.value?n.id:void 0),i=C(()=>{if(r&&r.value==="tooltip")return n.open&&n.id?n.id:void 0}),a=C(()=>{if(r&&r.value!=="tooltip")return r.value}),l=C(()=>a.value?`${n.open}`:void 0);let u;const c=["onMouseenter","onMouseleave","onClick","onKeydown","onFocus","onBlur","onContextmenu"];return Ke(()=>{he(()=>n.virtualRef,f=>{f&&(o.value=sr(f))},{immediate:!0}),he(o,(f,d)=>{u==null||u(),u=void 0,ar(f)&&(c.forEach(v=>{var p;const h=n[v];h&&(f.addEventListener(v.slice(2).toLowerCase(),h),(p=d==null?void 0:d.removeEventListener)==null||p.call(d,v.slice(2).toLowerCase(),h))}),u=he([s,i,a,l],v=>{["aria-controls","aria-describedby","aria-haspopup","aria-expanded"].forEach((p,h)=>{qn(v[h])?f.removeAttribute(p):f.setAttribute(p,v[h])})},{immediate:!0})),ar(d)&&["aria-controls","aria-describedby","aria-haspopup","aria-expanded"].forEach(v=>d.removeAttribute(v))},{immediate:!0})}),St(()=>{if(u==null||u(),u=void 0,o.value&&ar(o.value)){const f=o.value;c.forEach(d=>{const v=n[d];v&&f.removeEventListener(d.slice(2).toLowerCase(),v)}),o.value=void 0}}),t({triggerRef:o}),(f,d)=>f.virtualTriggering?ae("v-if",!0):(L(),fe(g(Q4),En({key:0},f.$attrs,{"aria-controls":g(s),"aria-describedby":g(i),"aria-expanded":g(l),"aria-haspopup":g(a)}),{default:ce(()=>[de(f.$slots,"default")]),_:3},16,["aria-controls","aria-describedby","aria-expanded","aria-haspopup"]))}});var nR=Ne(tR,[["__file","trigger.vue"]]);const Dl="focus-trap.focus-after-trapped",Vl="focus-trap.focus-after-released",rR="focus-trap.focusout-prevented",Cp={cancelable:!0,bubbles:!1},oR={cancelable:!0,bubbles:!1},Tp="focusAfterTrapped",Op="focusAfterReleased",Cg=Symbol("elFocusTrap"),jc=V(),ul=V(0),zc=V(0);let Li=0;const Tg=e=>{const t=[],n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:r=>{const o=r.tagName==="INPUT"&&r.type==="hidden";return r.disabled||r.hidden||o?NodeFilter.FILTER_SKIP:r.tabIndex>=0||r===document.activeElement?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;n.nextNode();)t.push(n.currentNode);return t},Ap=(e,t)=>{for(const n of e)if(!sR(n,t))return n},sR=(e,t)=>{if(getComputedStyle(e).visibility==="hidden")return!0;for(;e;){if(t&&e===t)return!1;if(getComputedStyle(e).display==="none")return!0;e=e.parentElement}return!1},iR=e=>{const t=Tg(e),n=Ap(t,e),r=Ap(t.reverse(),e);return[n,r]},aR=e=>e instanceof HTMLInputElement&&"select"in e,Tr=(e,t)=>{if(e&&e.focus){const n=document.activeElement;e.focus({preventScroll:!0}),zc.value=window.performance.now(),e!==n&&aR(e)&&t&&e.select()}};function Rp(e,t){const n=[...e],r=e.indexOf(t);return r!==-1&&n.splice(r,1),n}const lR=()=>{let e=[];return{push:r=>{const o=e[0];o&&r!==o&&o.pause(),e=Rp(e,r),e.unshift(r)},remove:r=>{var o,s;e=Rp(e,r),(s=(o=e[0])==null?void 0:o.resume)==null||s.call(o)}}},uR=(e,t=!1)=>{const n=document.activeElement;for(const r of e)if(Tr(r,t),document.activeElement!==n)return},xp=lR(),cR=()=>ul.value>zc.value,Mi=()=>{jc.value="pointer",ul.value=window.performance.now()},Ip=()=>{jc.value="keyboard",ul.value=window.performance.now()},fR=()=>(Ke(()=>{Li===0&&(document.addEventListener("mousedown",Mi),document.addEventListener("touchstart",Mi),document.addEventListener("keydown",Ip)),Li++}),St(()=>{Li--,Li<=0&&(document.removeEventListener("mousedown",Mi),document.removeEventListener("touchstart",Mi),document.removeEventListener("keydown",Ip))}),{focusReason:jc,lastUserFocusTimestamp:ul,lastAutomatedFocusTimestamp:zc}),$i=e=>new CustomEvent(rR,{...oR,detail:e}),dR=Z({name:"ElFocusTrap",inheritAttrs:!1,props:{loop:Boolean,trapped:Boolean,focusTrapEl:Object,focusStartEl:{type:[Object,String],default:"first"}},emits:[Tp,Op,"focusin","focusout","focusout-prevented","release-requested"],setup(e,{emit:t}){const n=V();let r,o;const{focusReason:s}=fR();cA(p=>{e.trapped&&!i.paused&&t("release-requested",p)});const i={paused:!1,pause(){this.paused=!0},resume(){this.paused=!1}},a=p=>{if(!e.loop&&!e.trapped||i.paused)return;const{key:h,altKey:b,ctrlKey:m,metaKey:S,currentTarget:_,shiftKey:w}=p,{loop:y}=e,A=h===_n.tab&&!b&&!m&&!S,R=document.activeElement;if(A&&R){const N=_,[I,x]=iR(N);if(I&&x){if(!w&&R===x){const $=$i({focusReason:s.value});t("focusout-prevented",$),$.defaultPrevented||(p.preventDefault(),y&&Tr(I,!0))}else if(w&&[I,N].includes(R)){const $=$i({focusReason:s.value});t("focusout-prevented",$),$.defaultPrevented||(p.preventDefault(),y&&Tr(x,!0))}}else if(R===N){const $=$i({focusReason:s.value});t("focusout-prevented",$),$.defaultPrevented||p.preventDefault()}}};ft(Cg,{focusTrapRef:n,onKeydown:a}),he(()=>e.focusTrapEl,p=>{p&&(n.value=p)},{immediate:!0}),he([n],([p],[h])=>{p&&(p.addEventListener("keydown",a),p.addEventListener("focusin",c),p.addEventListener("focusout",f)),h&&(h.removeEventListener("keydown",a),h.removeEventListener("focusin",c),h.removeEventListener("focusout",f))});const l=p=>{t(Tp,p)},u=p=>t(Op,p),c=p=>{const h=g(n);if(!h)return;const b=p.target,m=p.relatedTarget,S=b&&h.contains(b);e.trapped||m&&h.contains(m)||(r=m),S&&t("focusin",p),!i.paused&&e.trapped&&(S?o=b:Tr(o,!0))},f=p=>{const h=g(n);if(!(i.paused||!h))if(e.trapped){const b=p.relatedTarget;!qn(b)&&!h.contains(b)&&setTimeout(()=>{if(!i.paused&&e.trapped){const m=$i({focusReason:s.value});t("focusout-prevented",m),m.defaultPrevented||Tr(o,!0)}},0)}else{const b=p.target;b&&h.contains(b)||t("focusout",p)}};async function d(){await Ie();const p=g(n);if(p){xp.push(i);const h=p.contains(document.activeElement)?r:document.activeElement;if(r=h,!p.contains(h)){const m=new Event(Dl,Cp);p.addEventListener(Dl,l),p.dispatchEvent(m),m.defaultPrevented||Ie(()=>{let S=e.focusStartEl;Ae(S)||(Tr(S),document.activeElement!==S&&(S="first")),S==="first"&&uR(Tg(p),!0),(document.activeElement===h||S==="container")&&Tr(p)})}}}function v(){const p=g(n);if(p){p.removeEventListener(Dl,l);const h=new CustomEvent(Vl,{...Cp,detail:{focusReason:s.value}});p.addEventListener(Vl,u),p.dispatchEvent(h),!h.defaultPrevented&&(s.value=="keyboard"||!cR()||p.contains(document.activeElement))&&Tr(r??document.body),p.removeEventListener(Vl,u),xp.remove(i)}}return Ke(()=>{e.trapped&&d(),he(()=>e.trapped,p=>{p?d():v()})}),St(()=>{e.trapped&&v(),n.value&&(n.value.removeEventListener("keydown",a),n.value.removeEventListener("focusin",c),n.value.removeEventListener("focusout",f),n.value=void 0)}),{onKeydown:a}}});function pR(e,t,n,r,o,s){return de(e.$slots,"default",{handleKeydown:e.onKeydown})}var Hc=Ne(dR,[["render",pR],["__file","focus-trap.vue"]]);const hR=["fixed","absolute"],vR=xe({boundariesPadding:{type:Number,default:0},fallbackPlacements:{type:we(Array),default:void 0},gpuAcceleration:{type:Boolean,default:!0},offset:{type:Number,default:12},placement:{type:String,values:il,default:"bottom"},popperOptions:{type:we(Object),default:()=>({})},strategy:{type:String,values:hR,default:"absolute"}}),Og=xe({...vR,id:String,style:{type:we([String,Array,Object])},className:{type:we([String,Array,Object])},effect:{type:we(String),default:"dark"},visible:Boolean,enterable:{type:Boolean,default:!0},pure:Boolean,focusOnShow:{type:Boolean,default:!1},trapping:{type:Boolean,default:!1},popperClass:{type:we([String,Array,Object])},popperStyle:{type:we([String,Array,Object])},referenceEl:{type:we(Object)},triggerTargetEl:{type:we(Object)},stopPopperMouseEvent:{type:Boolean,default:!0},virtualTriggering:Boolean,zIndex:Number,...yr(["ariaLabel"])}),mR={mouseenter:e=>e instanceof MouseEvent,mouseleave:e=>e instanceof MouseEvent,focus:()=>!0,blur:()=>!0,close:()=>!0},gR=(e,t=[])=>{const{placement:n,strategy:r,popperOptions:o}=e,s={placement:n,strategy:r,...o,modifiers:[...yR(e),...t]};return wR(s,o==null?void 0:o.modifiers),s},bR=e=>{if(st)return sr(e)};function yR(e){const{offset:t,gpuAcceleration:n,fallbackPlacements:r}=e;return[{name:"offset",options:{offset:[0,t??12]}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5,fallbackPlacements:r}},{name:"computeStyles",options:{gpuAcceleration:n}}]}function wR(e,t){t&&(e.modifiers=[...e.modifiers,...t??[]])}const SR=0,ER=e=>{const{popperInstanceRef:t,contentRef:n,triggerRef:r,role:o}=Ee(Vc,void 0),s=V(),i=V(),a=C(()=>({name:"eventListeners",enabled:!!e.visible})),l=C(()=>{var m;const S=g(s),_=(m=g(i))!=null?m:SR;return{name:"arrow",enabled:!Rm(S),options:{element:S,padding:_}}}),u=C(()=>({onFirstUpdate:()=>{p()},...gR(e,[g(l),g(a)])})),c=C(()=>bR(e.referenceEl)||g(r)),{attributes:f,state:d,styles:v,update:p,forceUpdate:h,instanceRef:b}=aA(c,n,u);return he(b,m=>t.value=m),Ke(()=>{he(()=>{var m;return(m=g(c))==null?void 0:m.getBoundingClientRect()},()=>{p()})}),{attributes:f,arrowRef:s,contentRef:n,instanceRef:b,state:d,styles:v,role:o,forceUpdate:h,update:p}},_R=(e,{attributes:t,styles:n,role:r})=>{const{nextZIndex:o}=Fc(),s=$e("popper"),i=C(()=>g(t).popper),a=V(je(e.zIndex)?e.zIndex:o()),l=C(()=>[s.b(),s.is("pure",e.pure),s.is(e.effect),e.popperClass]),u=C(()=>[{zIndex:g(a)},g(n).popper,e.popperStyle||{}]),c=C(()=>r.value==="dialog"?"false":void 0),f=C(()=>g(n).arrow||{});return{ariaModal:c,arrowStyle:f,contentAttrs:i,contentClass:l,contentStyle:u,contentZIndex:a,updateZIndex:()=>{a.value=je(e.zIndex)?e.zIndex:o()}}},CR=(e,t)=>{const n=V(!1),r=V();return{focusStartRef:r,trapped:n,onFocusAfterReleased:u=>{var c;((c=u.detail)==null?void 0:c.focusReason)!=="pointer"&&(r.value="first",t("blur"))},onFocusAfterTrapped:()=>{t("focus")},onFocusInTrap:u=>{e.visible&&!n.value&&(u.target&&(r.value=u.target),n.value=!0)},onFocusoutPrevented:u=>{e.trapping||(u.detail.focusReason==="pointer"&&u.preventDefault(),n.value=!1)},onReleaseRequested:()=>{n.value=!1,t("close")}}},TR=Z({name:"ElPopperContent"}),OR=Z({...TR,props:Og,emits:mR,setup(e,{expose:t,emit:n}){const r=e,{focusStartRef:o,trapped:s,onFocusAfterReleased:i,onFocusAfterTrapped:a,onFocusInTrap:l,onFocusoutPrevented:u,onReleaseRequested:c}=CR(r,n),{attributes:f,arrowRef:d,contentRef:v,styles:p,instanceRef:h,role:b,update:m}=ER(r),{ariaModal:S,arrowStyle:_,contentAttrs:w,contentClass:y,contentStyle:A,updateZIndex:R}=_R(r,{styles:p,attributes:f,role:b}),N=Ee(ho,void 0);ft(yg,{arrowStyle:_,arrowRef:d,arrowOffset:V()}),N&&ft(ho,{...N,addInputId:ht,removeInputId:ht});let x;const k=(F=!0)=>{m(),F&&R()},$=()=>{k(!1),r.visible&&r.focusOnShow?s.value=!0:r.visible===!1&&(s.value=!1)};return Ke(()=>{he(()=>r.triggerTargetEl,(F,Y)=>{x==null||x(),x=void 0;const P=g(F||v.value),O=g(Y||v.value);ar(P)&&(x=he([b,()=>r.ariaLabel,S,()=>r.id],j=>{["role","aria-label","aria-modal","id"].forEach((Q,me)=>{qn(j[me])?P.removeAttribute(Q):P.setAttribute(Q,j[me])})},{immediate:!0})),O!==P&&ar(O)&&["role","aria-label","aria-modal","id"].forEach(j=>{O.removeAttribute(j)})},{immediate:!0}),he(()=>r.visible,$,{immediate:!0})}),St(()=>{x==null||x(),x=void 0}),t({popperContentRef:v,popperInstanceRef:h,updatePopper:k,contentStyle:A}),(F,Y)=>(L(),ee("div",En({ref_key:"contentRef",ref:v},g(w),{style:g(A),class:g(y),tabindex:"-1",onMouseenter:P=>F.$emit("mouseenter",P),onMouseleave:P=>F.$emit("mouseleave",P)}),[re(g(Hc),{trapped:g(s),"trap-on-focus-in":!0,"focus-trap-el":g(v),"focus-start-el":g(o),onFocusAfterTrapped:g(a),onFocusAfterReleased:g(i),onFocusin:g(l),onFocusoutPrevented:g(u),onReleaseRequested:g(c)},{default:ce(()=>[de(F.$slots,"default")]),_:3},8,["trapped","focus-trap-el","focus-start-el","onFocusAfterTrapped","onFocusAfterReleased","onFocusin","onFocusoutPrevented","onReleaseRequested"])],16,["onMouseenter","onMouseleave"]))}});var AR=Ne(OR,[["__file","content.vue"]]);const RR=yt(G4),Uc=Symbol("elTooltip"),Gt=xe({...pA,...Og,appendTo:{type:we([String,Object])},content:{type:String,default:""},rawContent:Boolean,persistent:Boolean,visible:{type:we(Boolean),default:null},transition:String,teleported:{type:Boolean,default:!0},disabled:Boolean,...yr(["ariaLabel"])}),ei=xe({..._g,disabled:Boolean,trigger:{type:we([String,Array]),default:"hover"},triggerKeys:{type:we(Array),default:()=>[_n.enter,_n.space]}}),{useModelToggleProps:xR,useModelToggleEmits:IR,useModelToggle:PR}=oO("visible"),NR=xe({...wg,...xR,...Gt,...ei,...Sg,showArrow:{type:Boolean,default:!0}}),LR=[...IR,"before-show","before-hide","show","hide","open","close"],MR=(e,t)=>pe(e)?e.includes(t):e===t,Oo=(e,t,n)=>r=>{MR(g(e),t)&&n(r)},$R=Z({name:"ElTooltipTrigger"}),kR=Z({...$R,props:ei,setup(e,{expose:t}){const n=e,r=$e("tooltip"),{controlled:o,id:s,open:i,onOpen:a,onClose:l,onToggle:u}=Ee(Uc,void 0),c=V(null),f=()=>{if(g(o)||n.disabled)return!0},d=Jt(n,"trigger"),v=Qn(f,Oo(d,"hover",a)),p=Qn(f,Oo(d,"hover",l)),h=Qn(f,Oo(d,"click",w=>{w.button===0&&u(w)})),b=Qn(f,Oo(d,"focus",a)),m=Qn(f,Oo(d,"focus",l)),S=Qn(f,Oo(d,"contextmenu",w=>{w.preventDefault(),u(w)})),_=Qn(f,w=>{const{code:y}=w;n.triggerKeys.includes(y)&&(w.preventDefault(),u(w))});return t({triggerRef:c}),(w,y)=>(L(),fe(g(nR),{id:g(s),"virtual-ref":w.virtualRef,open:g(i),"virtual-triggering":w.virtualTriggering,class:U(g(r).e("trigger")),onBlur:g(m),onClick:g(h),onContextmenu:g(S),onFocus:g(b),onMouseenter:g(v),onMouseleave:g(p),onKeydown:g(_)},{default:ce(()=>[de(w.$slots,"default")]),_:3},8,["id","virtual-ref","open","virtual-triggering","class","onBlur","onClick","onContextmenu","onFocus","onMouseenter","onMouseleave","onKeydown"]))}});var FR=Ne(kR,[["__file","trigger.vue"]]);const BR=xe({to:{type:we([String,Object]),required:!0},disabled:Boolean}),DR=Z({__name:"teleport",props:BR,setup(e){return(t,n)=>t.disabled?de(t.$slots,"default",{key:0}):(L(),fe(jy,{key:1,to:t.to},[de(t.$slots,"default")],8,["to"]))}});var VR=Ne(DR,[["__file","teleport.vue"]]);const Ag=yt(VR),jR=Z({name:"ElTooltipContent",inheritAttrs:!1}),zR=Z({...jR,props:Gt,setup(e,{expose:t}){const n=e,{selector:r}=ig(),o=$e("tooltip"),s=V(null);let i;const{controlled:a,id:l,open:u,trigger:c,onClose:f,onOpen:d,onShow:v,onHide:p,onBeforeShow:h,onBeforeHide:b}=Ee(Uc,void 0),m=C(()=>n.transition||`${o.namespace.value}-fade-in-linear`),S=C(()=>n.persistent);St(()=>{i==null||i()});const _=C(()=>g(S)?!0:g(u)),w=C(()=>n.disabled?!1:g(u)),y=C(()=>n.appendTo||r.value),A=C(()=>{var O;return(O=n.style)!=null?O:{}}),R=V(!0),N=()=>{p(),R.value=!0},I=()=>{if(g(a))return!0},x=Qn(I,()=>{n.enterable&&g(c)==="hover"&&d()}),k=Qn(I,()=>{g(c)==="hover"&&f()}),$=()=>{var O,j;(j=(O=s.value)==null?void 0:O.updatePopper)==null||j.call(O),h==null||h()},F=()=>{b==null||b()},Y=()=>{v(),i=H1(C(()=>{var O;return(O=s.value)==null?void 0:O.popperContentRef}),()=>{if(g(a))return;g(c)!=="hover"&&f()})},P=()=>{n.virtualTriggering||f()};return he(()=>g(u),O=>{O?R.value=!1:i==null||i()},{flush:"post"}),he(()=>n.content,()=>{var O,j;(j=(O=s.value)==null?void 0:O.updatePopper)==null||j.call(O)}),t({contentRef:s}),(O,j)=>(L(),fe(g(Ag),{disabled:!O.teleported,to:g(y)},{default:ce(()=>[re(Fr,{name:g(m),onAfterLeave:N,onBeforeEnter:$,onAfterEnter:Y,onBeforeLeave:F},{default:ce(()=>[g(_)?ct((L(),fe(g(AR),En({key:0,id:g(l),ref_key:"contentRef",ref:s},O.$attrs,{"aria-label":O.ariaLabel,"aria-hidden":R.value,"boundaries-padding":O.boundariesPadding,"fallback-placements":O.fallbackPlacements,"gpu-acceleration":O.gpuAcceleration,offset:O.offset,placement:O.placement,"popper-options":O.popperOptions,strategy:O.strategy,effect:O.effect,enterable:O.enterable,pure:O.pure,"popper-class":O.popperClass,"popper-style":[O.popperStyle,g(A)],"reference-el":O.referenceEl,"trigger-target-el":O.triggerTargetEl,visible:g(w),"z-index":O.zIndex,onMouseenter:g(x),onMouseleave:g(k),onBlur:P,onClose:g(f)}),{default:ce(()=>[de(O.$slots,"default")]),_:3},16,["id","aria-label","aria-hidden","boundaries-padding","fallback-placements","gpu-acceleration","offset","placement","popper-options","strategy","effect","enterable","pure","popper-class","popper-style","reference-el","trigger-target-el","visible","z-index","onMouseenter","onMouseleave","onClose"])),[[en,g(w)]]):ae("v-if",!0)]),_:3},8,["name"])]),_:3},8,["disabled","to"]))}});var HR=Ne(zR,[["__file","content.vue"]]);const UR=Z({name:"ElTooltip"}),KR=Z({...UR,props:NR,emits:LR,setup(e,{expose:t,emit:n}){const r=e;dA();const o=pr(),s=V(),i=V(),a=()=>{var m;const S=g(s);S&&((m=S.popperInstanceRef)==null||m.update())},l=V(!1),u=V(),{show:c,hide:f,hasUpdateHandler:d}=PR({indicator:l,toggleReason:u}),{onOpen:v,onClose:p}=hA({showAfter:Jt(r,"showAfter"),hideAfter:Jt(r,"hideAfter"),autoClose:Jt(r,"autoClose"),open:c,close:f}),h=C(()=>Bt(r.visible)&&!d.value);ft(Uc,{controlled:h,id:o,open:Mr(l),trigger:Jt(r,"trigger"),onOpen:m=>{v(m)},onClose:m=>{p(m)},onToggle:m=>{g(l)?p(m):v(m)},onShow:()=>{n("show",u.value)},onHide:()=>{n("hide",u.value)},onBeforeShow:()=>{n("before-show",u.value)},onBeforeHide:()=>{n("before-hide",u.value)},updatePopper:a}),he(()=>r.disabled,m=>{m&&l.value&&(l.value=!1)});const b=m=>{var S,_;const w=(_=(S=i.value)==null?void 0:S.contentRef)==null?void 0:_.popperContentRef,y=(m==null?void 0:m.relatedTarget)||document.activeElement;return w&&w.contains(y)};return oc(()=>l.value&&f()),t({popperRef:s,contentRef:i,isFocusInsideContent:b,updatePopper:a,onOpen:v,onClose:p,hide:f}),(m,S)=>(L(),fe(g(RR),{ref_key:"popperRef",ref:s,role:m.role},{default:ce(()=>[re(FR,{disabled:m.disabled,trigger:m.trigger,"trigger-keys":m.triggerKeys,"virtual-ref":m.virtualRef,"virtual-triggering":m.virtualTriggering},{default:ce(()=>[m.$slots.default?de(m.$slots,"default",{key:0}):ae("v-if",!0)]),_:3},8,["disabled","trigger","trigger-keys","virtual-ref","virtual-triggering"]),re(HR,{ref_key:"contentRef",ref:i,"aria-label":m.ariaLabel,"boundaries-padding":m.boundariesPadding,content:m.content,disabled:m.disabled,effect:m.effect,enterable:m.enterable,"fallback-placements":m.fallbackPlacements,"hide-after":m.hideAfter,"gpu-acceleration":m.gpuAcceleration,offset:m.offset,persistent:m.persistent,"popper-class":m.popperClass,"popper-style":m.popperStyle,placement:m.placement,"popper-options":m.popperOptions,pure:m.pure,"raw-content":m.rawContent,"reference-el":m.referenceEl,"trigger-target-el":m.triggerTargetEl,"show-after":m.showAfter,strategy:m.strategy,teleported:m.teleported,transition:m.transition,"virtual-triggering":m.virtualTriggering,"z-index":m.zIndex,"append-to":m.appendTo},{default:ce(()=>[de(m.$slots,"content",{},()=>[m.rawContent?(L(),ee("span",{key:0,innerHTML:m.content},null,8,["innerHTML"])):(L(),ee("span",{key:1},He(m.content),1))]),m.showArrow?(L(),fe(g(X4),{key:0,"arrow-offset":m.arrowOffset},null,8,["arrow-offset"])):ae("v-if",!0)]),_:3},8,["aria-label","boundaries-padding","content","disabled","effect","enterable","fallback-placements","hide-after","gpu-acceleration","offset","persistent","popper-class","popper-style","placement","popper-options","pure","raw-content","reference-el","trigger-target-el","show-after","strategy","teleported","transition","virtual-triggering","z-index","append-to"])]),_:3},8,["role"]))}});var qR=Ne(KR,[["__file","tooltip.vue"]]);const Rg=yt(qR),WR=xe({value:{type:[String,Number],default:""},max:{type:Number,default:99},isDot:Boolean,hidden:Boolean,type:{type:String,values:["primary","success","warning","info","danger"],default:"danger"},showZero:{type:Boolean,default:!0},color:String,badgeStyle:{type:we([String,Object,Array])},offset:{type:we(Array),default:[0,0]},badgeClass:{type:String}}),GR=Z({name:"ElBadge"}),YR=Z({...GR,props:WR,setup(e,{expose:t}){const n=e,r=$e("badge"),o=C(()=>n.isDot?"":je(n.value)&&je(n.max)?n.max{var i,a,l,u,c;return[{backgroundColor:n.color,marginRight:pn(-((a=(i=n.offset)==null?void 0:i[0])!=null?a:0)),marginTop:pn((u=(l=n.offset)==null?void 0:l[1])!=null?u:0)},(c=n.badgeStyle)!=null?c:{}]});return t({content:o}),(i,a)=>(L(),ee("div",{class:U(g(r).b())},[de(i.$slots,"default"),re(Fr,{name:`${g(r).namespace.value}-zoom-in-center`,persisted:""},{default:ce(()=>[ct(le("sup",{class:U([g(r).e("content"),g(r).em("content",i.type),g(r).is("fixed",!!i.$slots.default),g(r).is("dot",i.isDot),g(r).is("hide-zero",!i.showZero&&n.value===0),i.badgeClass]),style:ot(g(s)),textContent:He(g(o))},null,14,["textContent"]),[[en,!i.hidden&&(g(o)||i.isDot)]])]),_:1},8,["name"])],2))}});var JR=Ne(YR,[["__file","badge.vue"]]);const XR=yt(JR),xg=Symbol("buttonGroupContextKey"),ZR=(e,t)=>{xs({from:"type.text",replacement:"link",version:"3.0.0",scope:"props",ref:"https://element-plus.org/en-US/component/button.html#button-attributes"},C(()=>e.type==="text"));const n=Ee(xg,void 0),r=ll("button"),{form:o}=Vr(),s=In(C(()=>n==null?void 0:n.size)),i=ns(),a=V(),l=go(),u=C(()=>e.type||(n==null?void 0:n.type)||""),c=C(()=>{var p,h,b;return(b=(h=e.autoInsertSpace)!=null?h:(p=r.value)==null?void 0:p.autoInsertSpace)!=null?b:!1}),f=C(()=>e.tag==="button"?{ariaDisabled:i.value||e.loading,disabled:i.value||e.loading,autofocus:e.autofocus,type:e.nativeType}:{}),d=C(()=>{var p;const h=(p=l.default)==null?void 0:p.call(l);if(c.value&&(h==null?void 0:h.length)===1){const b=h[0];if((b==null?void 0:b.type)===Zo){const m=b.children;return new RegExp("^\\p{Unified_Ideograph}{2}$","u").test(m.trim())}}return!1});return{_disabled:i,_size:s,_type:u,_ref:a,_props:f,shouldAddSpace:d,handleClick:p=>{if(i.value||e.loading){p.stopPropagation();return}e.nativeType==="reset"&&(o==null||o.resetFields()),t("click",p)}}},QR=["default","primary","success","warning","info","danger","text",""],ex=["button","submit","reset"],Lu=xe({size:So,disabled:Boolean,type:{type:String,values:QR,default:""},icon:{type:jt},nativeType:{type:String,values:ex,default:"button"},loading:Boolean,loadingIcon:{type:jt,default:()=>Js},plain:Boolean,text:Boolean,link:Boolean,bg:Boolean,autofocus:Boolean,round:Boolean,circle:Boolean,color:String,dark:Boolean,autoInsertSpace:{type:Boolean,default:void 0},tag:{type:we([String,Object]),default:"button"}}),tx={click:e=>e instanceof MouseEvent};function Tt(e,t){nx(e)&&(e="100%");var n=rx(e);return e=t===360?e:Math.min(t,Math.max(0,parseFloat(e))),n&&(e=parseInt(String(e*t),10)/100),Math.abs(e-t)<1e-6?1:(t===360?e=(e<0?e%t+t:e%t)/parseFloat(String(t)):e=e%t/parseFloat(String(t)),e)}function ki(e){return Math.min(1,Math.max(0,e))}function nx(e){return typeof e=="string"&&e.indexOf(".")!==-1&&parseFloat(e)===1}function rx(e){return typeof e=="string"&&e.indexOf("%")!==-1}function Ig(e){return e=parseFloat(e),(isNaN(e)||e<0||e>1)&&(e=1),e}function Fi(e){return e<=1?"".concat(Number(e)*100,"%"):e}function to(e){return e.length===1?"0"+e:String(e)}function ox(e,t,n){return{r:Tt(e,255)*255,g:Tt(t,255)*255,b:Tt(n,255)*255}}function Pp(e,t,n){e=Tt(e,255),t=Tt(t,255),n=Tt(n,255);var r=Math.max(e,t,n),o=Math.min(e,t,n),s=0,i=0,a=(r+o)/2;if(r===o)i=0,s=0;else{var l=r-o;switch(i=a>.5?l/(2-r-o):l/(r+o),r){case e:s=(t-n)/l+(t1&&(n-=1),n<1/6?e+(t-e)*(6*n):n<1/2?t:n<2/3?e+(t-e)*(2/3-n)*6:e}function sx(e,t,n){var r,o,s;if(e=Tt(e,360),t=Tt(t,100),n=Tt(n,100),t===0)o=n,s=n,r=n;else{var i=n<.5?n*(1+t):n+t-n*t,a=2*n-i;r=jl(a,i,e+1/3),o=jl(a,i,e),s=jl(a,i,e-1/3)}return{r:r*255,g:o*255,b:s*255}}function Np(e,t,n){e=Tt(e,255),t=Tt(t,255),n=Tt(n,255);var r=Math.max(e,t,n),o=Math.min(e,t,n),s=0,i=r,a=r-o,l=r===0?0:a/r;if(r===o)s=0;else{switch(r){case e:s=(t-n)/a+(t>16,g:(e&65280)>>8,b:e&255}}var Mu={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",goldenrod:"#daa520",gold:"#ffd700",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavenderblush:"#fff0f5",lavender:"#e6e6fa",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",rebeccapurple:"#663399",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};function cx(e){var t={r:0,g:0,b:0},n=1,r=null,o=null,s=null,i=!1,a=!1;return typeof e=="string"&&(e=px(e)),typeof e=="object"&&(Xn(e.r)&&Xn(e.g)&&Xn(e.b)?(t=ox(e.r,e.g,e.b),i=!0,a=String(e.r).substr(-1)==="%"?"prgb":"rgb"):Xn(e.h)&&Xn(e.s)&&Xn(e.v)?(r=Fi(e.s),o=Fi(e.v),t=ix(e.h,r,o),i=!0,a="hsv"):Xn(e.h)&&Xn(e.s)&&Xn(e.l)&&(r=Fi(e.s),s=Fi(e.l),t=sx(e.h,r,s),i=!0,a="hsl"),Object.prototype.hasOwnProperty.call(e,"a")&&(n=e.a)),n=Ig(n),{ok:i,format:e.format||a,r:Math.min(255,Math.max(t.r,0)),g:Math.min(255,Math.max(t.g,0)),b:Math.min(255,Math.max(t.b,0)),a:n}}var fx="[-\\+]?\\d+%?",dx="[-\\+]?\\d*\\.\\d+%?",Nr="(?:".concat(dx,")|(?:").concat(fx,")"),zl="[\\s|\\(]+(".concat(Nr,")[,|\\s]+(").concat(Nr,")[,|\\s]+(").concat(Nr,")\\s*\\)?"),Hl="[\\s|\\(]+(".concat(Nr,")[,|\\s]+(").concat(Nr,")[,|\\s]+(").concat(Nr,")[,|\\s]+(").concat(Nr,")\\s*\\)?"),gn={CSS_UNIT:new RegExp(Nr),rgb:new RegExp("rgb"+zl),rgba:new RegExp("rgba"+Hl),hsl:new RegExp("hsl"+zl),hsla:new RegExp("hsla"+Hl),hsv:new RegExp("hsv"+zl),hsva:new RegExp("hsva"+Hl),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/};function px(e){if(e=e.trim().toLowerCase(),e.length===0)return!1;var t=!1;if(Mu[e])e=Mu[e],t=!0;else if(e==="transparent")return{r:0,g:0,b:0,a:0,format:"name"};var n=gn.rgb.exec(e);return n?{r:n[1],g:n[2],b:n[3]}:(n=gn.rgba.exec(e),n?{r:n[1],g:n[2],b:n[3],a:n[4]}:(n=gn.hsl.exec(e),n?{h:n[1],s:n[2],l:n[3]}:(n=gn.hsla.exec(e),n?{h:n[1],s:n[2],l:n[3],a:n[4]}:(n=gn.hsv.exec(e),n?{h:n[1],s:n[2],v:n[3]}:(n=gn.hsva.exec(e),n?{h:n[1],s:n[2],v:n[3],a:n[4]}:(n=gn.hex8.exec(e),n?{r:Wt(n[1]),g:Wt(n[2]),b:Wt(n[3]),a:Mp(n[4]),format:t?"name":"hex8"}:(n=gn.hex6.exec(e),n?{r:Wt(n[1]),g:Wt(n[2]),b:Wt(n[3]),format:t?"name":"hex"}:(n=gn.hex4.exec(e),n?{r:Wt(n[1]+n[1]),g:Wt(n[2]+n[2]),b:Wt(n[3]+n[3]),a:Mp(n[4]+n[4]),format:t?"name":"hex8"}:(n=gn.hex3.exec(e),n?{r:Wt(n[1]+n[1]),g:Wt(n[2]+n[2]),b:Wt(n[3]+n[3]),format:t?"name":"hex"}:!1)))))))))}function Xn(e){return!!gn.CSS_UNIT.exec(String(e))}var hx=function(){function e(t,n){t===void 0&&(t=""),n===void 0&&(n={});var r;if(t instanceof e)return t;typeof t=="number"&&(t=ux(t)),this.originalInput=t;var o=cx(t);this.originalInput=t,this.r=o.r,this.g=o.g,this.b=o.b,this.a=o.a,this.roundA=Math.round(100*this.a)/100,this.format=(r=n.format)!==null&&r!==void 0?r:o.format,this.gradientType=n.gradientType,this.r<1&&(this.r=Math.round(this.r)),this.g<1&&(this.g=Math.round(this.g)),this.b<1&&(this.b=Math.round(this.b)),this.isValid=o.ok}return e.prototype.isDark=function(){return this.getBrightness()<128},e.prototype.isLight=function(){return!this.isDark()},e.prototype.getBrightness=function(){var t=this.toRgb();return(t.r*299+t.g*587+t.b*114)/1e3},e.prototype.getLuminance=function(){var t=this.toRgb(),n,r,o,s=t.r/255,i=t.g/255,a=t.b/255;return s<=.03928?n=s/12.92:n=Math.pow((s+.055)/1.055,2.4),i<=.03928?r=i/12.92:r=Math.pow((i+.055)/1.055,2.4),a<=.03928?o=a/12.92:o=Math.pow((a+.055)/1.055,2.4),.2126*n+.7152*r+.0722*o},e.prototype.getAlpha=function(){return this.a},e.prototype.setAlpha=function(t){return this.a=Ig(t),this.roundA=Math.round(100*this.a)/100,this},e.prototype.isMonochrome=function(){var t=this.toHsl().s;return t===0},e.prototype.toHsv=function(){var t=Np(this.r,this.g,this.b);return{h:t.h*360,s:t.s,v:t.v,a:this.a}},e.prototype.toHsvString=function(){var t=Np(this.r,this.g,this.b),n=Math.round(t.h*360),r=Math.round(t.s*100),o=Math.round(t.v*100);return this.a===1?"hsv(".concat(n,", ").concat(r,"%, ").concat(o,"%)"):"hsva(".concat(n,", ").concat(r,"%, ").concat(o,"%, ").concat(this.roundA,")")},e.prototype.toHsl=function(){var t=Pp(this.r,this.g,this.b);return{h:t.h*360,s:t.s,l:t.l,a:this.a}},e.prototype.toHslString=function(){var t=Pp(this.r,this.g,this.b),n=Math.round(t.h*360),r=Math.round(t.s*100),o=Math.round(t.l*100);return this.a===1?"hsl(".concat(n,", ").concat(r,"%, ").concat(o,"%)"):"hsla(".concat(n,", ").concat(r,"%, ").concat(o,"%, ").concat(this.roundA,")")},e.prototype.toHex=function(t){return t===void 0&&(t=!1),Lp(this.r,this.g,this.b,t)},e.prototype.toHexString=function(t){return t===void 0&&(t=!1),"#"+this.toHex(t)},e.prototype.toHex8=function(t){return t===void 0&&(t=!1),ax(this.r,this.g,this.b,this.a,t)},e.prototype.toHex8String=function(t){return t===void 0&&(t=!1),"#"+this.toHex8(t)},e.prototype.toHexShortString=function(t){return t===void 0&&(t=!1),this.a===1?this.toHexString(t):this.toHex8String(t)},e.prototype.toRgb=function(){return{r:Math.round(this.r),g:Math.round(this.g),b:Math.round(this.b),a:this.a}},e.prototype.toRgbString=function(){var t=Math.round(this.r),n=Math.round(this.g),r=Math.round(this.b);return this.a===1?"rgb(".concat(t,", ").concat(n,", ").concat(r,")"):"rgba(".concat(t,", ").concat(n,", ").concat(r,", ").concat(this.roundA,")")},e.prototype.toPercentageRgb=function(){var t=function(n){return"".concat(Math.round(Tt(n,255)*100),"%")};return{r:t(this.r),g:t(this.g),b:t(this.b),a:this.a}},e.prototype.toPercentageRgbString=function(){var t=function(n){return Math.round(Tt(n,255)*100)};return this.a===1?"rgb(".concat(t(this.r),"%, ").concat(t(this.g),"%, ").concat(t(this.b),"%)"):"rgba(".concat(t(this.r),"%, ").concat(t(this.g),"%, ").concat(t(this.b),"%, ").concat(this.roundA,")")},e.prototype.toName=function(){if(this.a===0)return"transparent";if(this.a<1)return!1;for(var t="#"+Lp(this.r,this.g,this.b,!1),n=0,r=Object.entries(Mu);n=0,s=!n&&o&&(t.startsWith("hex")||t==="name");return s?t==="name"&&this.a===0?this.toName():this.toRgbString():(t==="rgb"&&(r=this.toRgbString()),t==="prgb"&&(r=this.toPercentageRgbString()),(t==="hex"||t==="hex6")&&(r=this.toHexString()),t==="hex3"&&(r=this.toHexString(!0)),t==="hex4"&&(r=this.toHex8String(!0)),t==="hex8"&&(r=this.toHex8String()),t==="name"&&(r=this.toName()),t==="hsl"&&(r=this.toHslString()),t==="hsv"&&(r=this.toHsvString()),r||this.toHexString())},e.prototype.toNumber=function(){return(Math.round(this.r)<<16)+(Math.round(this.g)<<8)+Math.round(this.b)},e.prototype.clone=function(){return new e(this.toString())},e.prototype.lighten=function(t){t===void 0&&(t=10);var n=this.toHsl();return n.l+=t/100,n.l=ki(n.l),new e(n)},e.prototype.brighten=function(t){t===void 0&&(t=10);var n=this.toRgb();return n.r=Math.max(0,Math.min(255,n.r-Math.round(255*-(t/100)))),n.g=Math.max(0,Math.min(255,n.g-Math.round(255*-(t/100)))),n.b=Math.max(0,Math.min(255,n.b-Math.round(255*-(t/100)))),new e(n)},e.prototype.darken=function(t){t===void 0&&(t=10);var n=this.toHsl();return n.l-=t/100,n.l=ki(n.l),new e(n)},e.prototype.tint=function(t){return t===void 0&&(t=10),this.mix("white",t)},e.prototype.shade=function(t){return t===void 0&&(t=10),this.mix("black",t)},e.prototype.desaturate=function(t){t===void 0&&(t=10);var n=this.toHsl();return n.s-=t/100,n.s=ki(n.s),new e(n)},e.prototype.saturate=function(t){t===void 0&&(t=10);var n=this.toHsl();return n.s+=t/100,n.s=ki(n.s),new e(n)},e.prototype.greyscale=function(){return this.desaturate(100)},e.prototype.spin=function(t){var n=this.toHsl(),r=(n.h+t)%360;return n.h=r<0?360+r:r,new e(n)},e.prototype.mix=function(t,n){n===void 0&&(n=50);var r=this.toRgb(),o=new e(t).toRgb(),s=n/100,i={r:(o.r-r.r)*s+r.r,g:(o.g-r.g)*s+r.g,b:(o.b-r.b)*s+r.b,a:(o.a-r.a)*s+r.a};return new e(i)},e.prototype.analogous=function(t,n){t===void 0&&(t=6),n===void 0&&(n=30);var r=this.toHsl(),o=360/n,s=[this];for(r.h=(r.h-(o*t>>1)+720)%360;--t;)r.h=(r.h+o)%360,s.push(new e(r));return s},e.prototype.complement=function(){var t=this.toHsl();return t.h=(t.h+180)%360,new e(t)},e.prototype.monochromatic=function(t){t===void 0&&(t=6);for(var n=this.toHsv(),r=n.h,o=n.s,s=n.v,i=[],a=1/t;t--;)i.push(new e({h:r,s:o,v:s})),s=(s+a)%1;return i},e.prototype.splitcomplement=function(){var t=this.toHsl(),n=t.h;return[this,new e({h:(n+72)%360,s:t.s,l:t.l}),new e({h:(n+216)%360,s:t.s,l:t.l})]},e.prototype.onBackground=function(t){var n=this.toRgb(),r=new e(t).toRgb(),o=n.a+r.a*(1-n.a);return new e({r:(n.r*n.a+r.r*r.a*(1-n.a))/o,g:(n.g*n.a+r.g*r.a*(1-n.a))/o,b:(n.b*n.a+r.b*r.a*(1-n.a))/o,a:o})},e.prototype.triad=function(){return this.polyad(3)},e.prototype.tetrad=function(){return this.polyad(4)},e.prototype.polyad=function(t){for(var n=this.toHsl(),r=n.h,o=[this],s=360/t,i=1;i{let r={},o=e.color;if(o){const s=o.match(/var\((.*?)\)/);s&&(o=window.getComputedStyle(window.document.documentElement).getPropertyValue(s[1]));const i=new hx(o),a=e.dark?i.tint(20).toString():_r(i,20);if(e.plain)r=n.cssVarBlock({"bg-color":e.dark?_r(i,90):i.tint(90).toString(),"text-color":o,"border-color":e.dark?_r(i,50):i.tint(50).toString(),"hover-text-color":`var(${n.cssVarName("color-white")})`,"hover-bg-color":o,"hover-border-color":o,"active-bg-color":a,"active-text-color":`var(${n.cssVarName("color-white")})`,"active-border-color":a}),t.value&&(r[n.cssVarBlockName("disabled-bg-color")]=e.dark?_r(i,90):i.tint(90).toString(),r[n.cssVarBlockName("disabled-text-color")]=e.dark?_r(i,50):i.tint(50).toString(),r[n.cssVarBlockName("disabled-border-color")]=e.dark?_r(i,80):i.tint(80).toString());else{const l=e.dark?_r(i,30):i.tint(30).toString(),u=i.isDark()?`var(${n.cssVarName("color-white")})`:`var(${n.cssVarName("color-black")})`;if(r=n.cssVarBlock({"bg-color":o,"text-color":u,"border-color":o,"hover-bg-color":l,"hover-text-color":u,"hover-border-color":l,"active-bg-color":a,"active-border-color":a}),t.value){const c=e.dark?_r(i,50):i.tint(50).toString();r[n.cssVarBlockName("disabled-bg-color")]=c,r[n.cssVarBlockName("disabled-text-color")]=e.dark?"rgba(255, 255, 255, 0.5)":`var(${n.cssVarName("color-white")})`,r[n.cssVarBlockName("disabled-border-color")]=c}}}return r})}const mx=Z({name:"ElButton"}),gx=Z({...mx,props:Lu,emits:tx,setup(e,{expose:t,emit:n}){const r=e,o=vx(r),s=$e("button"),{_ref:i,_size:a,_type:l,_disabled:u,_props:c,shouldAddSpace:f,handleClick:d}=ZR(r,n),v=C(()=>[s.b(),s.m(l.value),s.m(a.value),s.is("disabled",u.value),s.is("loading",r.loading),s.is("plain",r.plain),s.is("round",r.round),s.is("circle",r.circle),s.is("text",r.text),s.is("link",r.link),s.is("has-bg",r.bg)]);return t({ref:i,size:a,type:l,disabled:u,shouldAddSpace:f}),(p,h)=>(L(),fe(Xe(p.tag),En({ref_key:"_ref",ref:i},g(c),{class:g(v),style:g(o),onClick:g(d)}),{default:ce(()=>[p.loading?(L(),ee(nt,{key:0},[p.$slots.loading?de(p.$slots,"loading",{key:0}):(L(),fe(g(Je),{key:1,class:U(g(s).is("loading"))},{default:ce(()=>[(L(),fe(Xe(p.loadingIcon)))]),_:1},8,["class"]))],64)):p.icon||p.$slots.icon?(L(),fe(g(Je),{key:1},{default:ce(()=>[p.icon?(L(),fe(Xe(p.icon),{key:0})):de(p.$slots,"icon",{key:1})]),_:3})):ae("v-if",!0),p.$slots.default?(L(),ee("span",{key:2,class:U({[g(s).em("text","expand")]:g(f)})},[de(p.$slots,"default")],2)):ae("v-if",!0)]),_:3},16,["class","style","onClick"]))}});var bx=Ne(gx,[["__file","button.vue"]]);const yx={size:Lu.size,type:Lu.type},wx=Z({name:"ElButtonGroup"}),Sx=Z({...wx,props:yx,setup(e){const t=e;ft(xg,wt({size:Jt(t,"size"),type:Jt(t,"type")}));const n=$e("button");return(r,o)=>(L(),ee("div",{class:U(g(n).b("group"))},[de(r.$slots,"default")],2))}});var Pg=Ne(Sx,[["__file","button-group.vue"]]);const Ex=yt(bx,{ButtonGroup:Pg});wo(Pg);const Or=new Map;if(st){let e;document.addEventListener("mousedown",t=>e=t),document.addEventListener("mouseup",t=>{if(e){for(const n of Or.values())for(const{documentHandler:r}of n)r(t,e);e=void 0}})}function $p(e,t){let n=[];return Array.isArray(t.arg)?n=t.arg:ar(t.arg)&&n.push(t.arg),function(r,o){const s=t.instance.popperRef,i=r.target,a=o==null?void 0:o.target,l=!t||!t.instance,u=!i||!a,c=e.contains(i)||e.contains(a),f=e===i,d=n.length&&n.some(p=>p==null?void 0:p.contains(i))||n.length&&n.includes(a),v=s&&(s.contains(i)||s.contains(a));l||u||c||f||d||v||t.value(r,o)}}const _x={beforeMount(e,t){Or.has(e)||Or.set(e,[]),Or.get(e).push({documentHandler:$p(e,t),bindingFn:t.value})},updated(e,t){Or.has(e)||Or.set(e,[]);const n=Or.get(e),r=n.findIndex(s=>s.bindingFn===t.oldValue),o={documentHandler:$p(e,t),bindingFn:t.value};r>=0?n.splice(r,1,o):n.push(o)},unmounted(e){Or.delete(e)}},Cx=100,Tx=600,kp={beforeMount(e,t){const n=t.value,{interval:r=Cx,delay:o=Tx}=ve(n)?{}:n;let s,i;const a=()=>ve(n)?n():n.handler(),l=()=>{i&&(clearTimeout(i),i=void 0),s&&(clearInterval(s),s=void 0)};e.addEventListener("mousedown",u=>{u.button===0&&(l(),a(),document.addEventListener("mouseup",()=>l(),{once:!0}),i=setTimeout(()=>{s=setInterval(()=>{a()},r)},o))})}},$u="_trap-focus-children",no=[],Fp=e=>{if(no.length===0)return;const t=no[no.length-1][$u];if(t.length>0&&e.code===_n.tab){if(t.length===1){e.preventDefault(),document.activeElement!==t[0]&&t[0].focus();return}const n=e.shiftKey,r=e.target===t[0],o=e.target===t[t.length-1];r&&n&&(e.preventDefault(),t[t.length-1].focus()),o&&!n&&(e.preventDefault(),t[0].focus())}},Ox={beforeMount(e){e[$u]=ld(e),no.push(e),no.length<=1&&document.addEventListener("keydown",Fp)},updated(e){Ie(()=>{e[$u]=ld(e)})},unmounted(){no.shift(),no.length===0&&document.removeEventListener("keydown",Fp)}},Ng={modelValue:{type:[Number,String,Boolean],default:void 0},label:{type:[String,Boolean,Number,Object],default:void 0},value:{type:[String,Boolean,Number,Object],default:void 0},indeterminate:Boolean,disabled:Boolean,checked:Boolean,name:{type:String,default:void 0},trueValue:{type:[String,Number],default:void 0},falseValue:{type:[String,Number],default:void 0},trueLabel:{type:[String,Number],default:void 0},falseLabel:{type:[String,Number],default:void 0},id:{type:String,default:void 0},border:Boolean,size:So,tabindex:[String,Number],validateEvent:{type:Boolean,default:!0},...yr(["ariaControls"])},Lg={[rt]:e=>Ae(e)||je(e)||Bt(e),change:e=>Ae(e)||je(e)||Bt(e)},rs=Symbol("checkboxGroupContextKey"),Ax=({model:e,isChecked:t})=>{const n=Ee(rs,void 0),r=C(()=>{var s,i;const a=(s=n==null?void 0:n.max)==null?void 0:s.value,l=(i=n==null?void 0:n.min)==null?void 0:i.value;return!Lt(a)&&e.value.length>=a&&!t.value||!Lt(l)&&e.value.length<=l&&t.value});return{isDisabled:ns(C(()=>(n==null?void 0:n.disabled.value)||r.value)),isLimitDisabled:r}},Rx=(e,{model:t,isLimitExceeded:n,hasOwnLabel:r,isDisabled:o,isLabeledByFormItem:s})=>{const i=Ee(rs,void 0),{formItem:a}=Vr(),{emit:l}=Ge();function u(p){var h,b,m,S;return[!0,e.trueValue,e.trueLabel].includes(p)?(b=(h=e.trueValue)!=null?h:e.trueLabel)!=null?b:!0:(S=(m=e.falseValue)!=null?m:e.falseLabel)!=null?S:!1}function c(p,h){l("change",u(p),h)}function f(p){if(n.value)return;const h=p.target;l("change",u(h.checked),p)}async function d(p){n.value||!r.value&&!o.value&&s.value&&(p.composedPath().some(m=>m.tagName==="LABEL")||(t.value=u([!1,e.falseValue,e.falseLabel].includes(t.value)),await Ie(),c(t.value,p)))}const v=C(()=>(i==null?void 0:i.validateEvent)||e.validateEvent);return he(()=>e.modelValue,()=>{v.value&&(a==null||a.validate("change").catch(p=>void 0))}),{handleChange:f,onClickRoot:d}},xx=e=>{const t=V(!1),{emit:n}=Ge(),r=Ee(rs,void 0),o=C(()=>Lt(r)===!1),s=V(!1),i=C({get(){var a,l;return o.value?(a=r==null?void 0:r.modelValue)==null?void 0:a.value:(l=e.modelValue)!=null?l:t.value},set(a){var l,u;o.value&&pe(a)?(s.value=((l=r==null?void 0:r.max)==null?void 0:l.value)!==void 0&&a.length>(r==null?void 0:r.max.value)&&a.length>i.value.length,s.value===!1&&((u=r==null?void 0:r.changeEvent)==null||u.call(r,a))):(n(rt,a),t.value=a)}});return{model:i,isGroup:o,isLimitExceeded:s}},Ix=(e,t,{model:n})=>{const r=Ee(rs,void 0),o=V(!1),s=C(()=>Cu(e.value)?e.label:e.value),i=C(()=>{const c=n.value;return Bt(c)?c:pe(c)?Oe(s.value)?c.map(Me).some(f=>Aa(f,s.value)):c.map(Me).includes(s.value):c!=null?c===e.trueValue||c===e.trueLabel:!!c}),a=In(C(()=>{var c;return(c=r==null?void 0:r.size)==null?void 0:c.value}),{prop:!0}),l=In(C(()=>{var c;return(c=r==null?void 0:r.size)==null?void 0:c.value})),u=C(()=>!!t.default||!Cu(s.value));return{checkboxButtonSize:a,isChecked:i,isFocused:o,checkboxSize:l,hasOwnLabel:u,actualValue:s}},Mg=(e,t)=>{const{formItem:n}=Vr(),{model:r,isGroup:o,isLimitExceeded:s}=xx(e),{isFocused:i,isChecked:a,checkboxButtonSize:l,checkboxSize:u,hasOwnLabel:c,actualValue:f}=Ix(e,t,{model:r}),{isDisabled:d}=Ax({model:r,isChecked:a}),{inputId:v,isLabeledByFormItem:p}=hi(e,{formItemContext:n,disableIdGeneration:c,disableIdManagement:o}),{handleChange:h,onClickRoot:b}=Rx(e,{model:r,isLimitExceeded:s,hasOwnLabel:c,isDisabled:d,isLabeledByFormItem:p});return(()=>{function S(){var _,w;pe(r.value)&&!r.value.includes(f.value)?r.value.push(f.value):r.value=(w=(_=e.trueValue)!=null?_:e.trueLabel)!=null?w:!0}e.checked&&S()})(),xs({from:"label act as value",replacement:"value",version:"3.0.0",scope:"el-checkbox",ref:"https://element-plus.org/en-US/component/checkbox.html"},C(()=>o.value&&Cu(e.value))),xs({from:"true-label",replacement:"true-value",version:"3.0.0",scope:"el-checkbox",ref:"https://element-plus.org/en-US/component/checkbox.html"},C(()=>!!e.trueLabel)),xs({from:"false-label",replacement:"false-value",version:"3.0.0",scope:"el-checkbox",ref:"https://element-plus.org/en-US/component/checkbox.html"},C(()=>!!e.falseLabel)),{inputId:v,isLabeledByFormItem:p,isChecked:a,isDisabled:d,isFocused:i,checkboxButtonSize:l,checkboxSize:u,hasOwnLabel:c,model:r,actualValue:f,handleChange:h,onClickRoot:b}},Px=Z({name:"ElCheckbox"}),Nx=Z({...Px,props:Ng,emits:Lg,setup(e){const t=e,n=go(),{inputId:r,isLabeledByFormItem:o,isChecked:s,isDisabled:i,isFocused:a,checkboxSize:l,hasOwnLabel:u,model:c,actualValue:f,handleChange:d,onClickRoot:v}=Mg(t,n),p=$e("checkbox"),h=C(()=>[p.b(),p.m(l.value),p.is("disabled",i.value),p.is("bordered",t.border),p.is("checked",s.value)]),b=C(()=>[p.e("input"),p.is("disabled",i.value),p.is("checked",s.value),p.is("indeterminate",t.indeterminate),p.is("focus",a.value)]);return(m,S)=>(L(),fe(Xe(!g(u)&&g(o)?"span":"label"),{class:U(g(h)),"aria-controls":m.indeterminate?m.ariaControls:null,onClick:g(v)},{default:ce(()=>{var _,w,y,A;return[le("span",{class:U(g(b))},[m.trueValue||m.falseValue||m.trueLabel||m.falseLabel?ct((L(),ee("input",{key:0,id:g(r),"onUpdate:modelValue":R=>Ue(c)?c.value=R:null,class:U(g(p).e("original")),type:"checkbox",indeterminate:m.indeterminate,name:m.name,tabindex:m.tabindex,disabled:g(i),"true-value":(w=(_=m.trueValue)!=null?_:m.trueLabel)!=null?w:!0,"false-value":(A=(y=m.falseValue)!=null?y:m.falseLabel)!=null?A:!1,onChange:g(d),onFocus:R=>a.value=!0,onBlur:R=>a.value=!1,onClick:tt(()=>{},["stop"])},null,42,["id","onUpdate:modelValue","indeterminate","name","tabindex","disabled","true-value","false-value","onChange","onFocus","onBlur","onClick"])),[[ya,g(c)]]):ct((L(),ee("input",{key:1,id:g(r),"onUpdate:modelValue":R=>Ue(c)?c.value=R:null,class:U(g(p).e("original")),type:"checkbox",indeterminate:m.indeterminate,disabled:g(i),value:g(f),name:m.name,tabindex:m.tabindex,onChange:g(d),onFocus:R=>a.value=!0,onBlur:R=>a.value=!1,onClick:tt(()=>{},["stop"])},null,42,["id","onUpdate:modelValue","indeterminate","disabled","value","name","tabindex","onChange","onFocus","onBlur","onClick"])),[[ya,g(c)]]),le("span",{class:U(g(p).e("inner"))},null,2)],2),g(u)?(L(),ee("span",{key:0,class:U(g(p).e("label"))},[de(m.$slots,"default"),m.$slots.default?ae("v-if",!0):(L(),ee(nt,{key:0},[Un(He(m.label),1)],64))],2)):ae("v-if",!0)]}),_:3},8,["class","aria-controls","onClick"]))}});var Lx=Ne(Nx,[["__file","checkbox.vue"]]);const Mx=Z({name:"ElCheckboxButton"}),$x=Z({...Mx,props:Ng,emits:Lg,setup(e){const t=e,n=go(),{isFocused:r,isChecked:o,isDisabled:s,checkboxButtonSize:i,model:a,actualValue:l,handleChange:u}=Mg(t,n),c=Ee(rs,void 0),f=$e("checkbox"),d=C(()=>{var p,h,b,m;const S=(h=(p=c==null?void 0:c.fill)==null?void 0:p.value)!=null?h:"";return{backgroundColor:S,borderColor:S,color:(m=(b=c==null?void 0:c.textColor)==null?void 0:b.value)!=null?m:"",boxShadow:S?`-1px 0 0 0 ${S}`:void 0}}),v=C(()=>[f.b("button"),f.bm("button",i.value),f.is("disabled",s.value),f.is("checked",o.value),f.is("focus",r.value)]);return(p,h)=>{var b,m,S,_;return L(),ee("label",{class:U(g(v))},[p.trueValue||p.falseValue||p.trueLabel||p.falseLabel?ct((L(),ee("input",{key:0,"onUpdate:modelValue":w=>Ue(a)?a.value=w:null,class:U(g(f).be("button","original")),type:"checkbox",name:p.name,tabindex:p.tabindex,disabled:g(s),"true-value":(m=(b=p.trueValue)!=null?b:p.trueLabel)!=null?m:!0,"false-value":(_=(S=p.falseValue)!=null?S:p.falseLabel)!=null?_:!1,onChange:g(u),onFocus:w=>r.value=!0,onBlur:w=>r.value=!1,onClick:tt(()=>{},["stop"])},null,42,["onUpdate:modelValue","name","tabindex","disabled","true-value","false-value","onChange","onFocus","onBlur","onClick"])),[[ya,g(a)]]):ct((L(),ee("input",{key:1,"onUpdate:modelValue":w=>Ue(a)?a.value=w:null,class:U(g(f).be("button","original")),type:"checkbox",name:p.name,tabindex:p.tabindex,disabled:g(s),value:g(l),onChange:g(u),onFocus:w=>r.value=!0,onBlur:w=>r.value=!1,onClick:tt(()=>{},["stop"])},null,42,["onUpdate:modelValue","name","tabindex","disabled","value","onChange","onFocus","onBlur","onClick"])),[[ya,g(a)]]),p.$slots.default||p.label?(L(),ee("span",{key:2,class:U(g(f).be("button","inner")),style:ot(g(o)?g(d):void 0)},[de(p.$slots,"default",{},()=>[Un(He(p.label),1)])],6)):ae("v-if",!0)],2)}}});var $g=Ne($x,[["__file","checkbox-button.vue"]]);const kx=xe({modelValue:{type:we(Array),default:()=>[]},disabled:Boolean,min:Number,max:Number,size:So,fill:String,textColor:String,tag:{type:String,default:"div"},validateEvent:{type:Boolean,default:!0},...yr(["ariaLabel"])}),Fx={[rt]:e=>pe(e),change:e=>pe(e)},Bx=Z({name:"ElCheckboxGroup"}),Dx=Z({...Bx,props:kx,emits:Fx,setup(e,{emit:t}){const n=e,r=$e("checkbox"),{formItem:o}=Vr(),{inputId:s,isLabeledByFormItem:i}=hi(n,{formItemContext:o}),a=async u=>{t(rt,u),await Ie(),t("change",u)},l=C({get(){return n.modelValue},set(u){a(u)}});return ft(rs,{...Im(vr(n),["size","min","max","disabled","validateEvent","fill","textColor"]),modelValue:l,changeEvent:a}),he(()=>n.modelValue,()=>{n.validateEvent&&(o==null||o.validate("change").catch(u=>void 0))}),(u,c)=>{var f;return L(),fe(Xe(u.tag),{id:g(s),class:U(g(r).b("group")),role:"group","aria-label":g(i)?void 0:u.ariaLabel||"checkbox-group","aria-labelledby":g(i)?(f=g(o))==null?void 0:f.labelId:void 0},{default:ce(()=>[de(u.$slots,"default")]),_:3},8,["id","class","aria-label","aria-labelledby"])}}});var kg=Ne(Dx,[["__file","checkbox-group.vue"]]);const NN=yt(Lx,{CheckboxButton:$g,CheckboxGroup:kg});wo($g);const LN=wo(kg),ku=xe({type:{type:String,values:["primary","success","info","warning","danger"],default:"primary"},closable:Boolean,disableTransitions:Boolean,hit:Boolean,color:String,size:{type:String,values:es},effect:{type:String,values:["dark","light","plain"],default:"light"},round:Boolean}),Vx={close:e=>e instanceof MouseEvent,click:e=>e instanceof MouseEvent},jx=Z({name:"ElTag"}),zx=Z({...jx,props:ku,emits:Vx,setup(e,{emit:t}){const n=e,r=In(),o=$e("tag"),s=C(()=>{const{type:u,hit:c,effect:f,closable:d,round:v}=n;return[o.b(),o.is("closable",d),o.m(u||"primary"),o.m(r.value),o.m(f),o.is("hit",c),o.is("round",v)]}),i=u=>{t("close",u)},a=u=>{t("click",u)},l=u=>{u.component.subTree.component.bum=null};return(u,c)=>u.disableTransitions?(L(),ee("span",{key:0,class:U(g(s)),style:ot({backgroundColor:u.color}),onClick:a},[le("span",{class:U(g(o).e("content"))},[de(u.$slots,"default")],2),u.closable?(L(),fe(g(Je),{key:0,class:U(g(o).e("close")),onClick:tt(i,["stop"])},{default:ce(()=>[re(g(Ys))]),_:1},8,["class","onClick"])):ae("v-if",!0)],6)):(L(),fe(Fr,{key:1,name:`${g(o).namespace.value}-zoom-in-center`,appear:"",onVnodeMounted:l},{default:ce(()=>[le("span",{class:U(g(s)),style:ot({backgroundColor:u.color}),onClick:a},[le("span",{class:U(g(o).e("content"))},[de(u.$slots,"default")],2),u.closable?(L(),fe(g(Je),{key:0,class:U(g(o).e("close")),onClick:tt(i,["stop"])},{default:ce(()=>[re(g(Ys))]),_:1},8,["class","onClick"])):ae("v-if",!0)],6)]),_:3},8,["name"]))}});var Hx=Ne(zx,[["__file","tag.vue"]]);const Ux=yt(Hx),Kx=xe({mask:{type:Boolean,default:!0},customMaskEvent:Boolean,overlayClass:{type:we([String,Array,Object])},zIndex:{type:we([String,Number])}}),qx={click:e=>e instanceof MouseEvent},Wx="overlay";var Gx=Z({name:"ElOverlay",props:Kx,emits:qx,setup(e,{slots:t,emit:n}){const r=$e(Wx),o=l=>{n("click",l)},{onClick:s,onMousedown:i,onMouseup:a}=kc(e.customMaskEvent?void 0:o);return()=>e.mask?re("div",{class:[r.b(),e.overlayClass],style:{zIndex:e.zIndex},onClick:s,onMousedown:i,onMouseup:a},[de(t,"default")],Zi.STYLE|Zi.CLASS|Zi.PROPS,["onClick","onMouseup","onMousedown"]):or("div",{class:e.overlayClass,style:{zIndex:e.zIndex,position:"fixed",top:"0px",right:"0px",bottom:"0px",left:"0px"}},[de(t,"default")])}});const Fg=Gx,Bg=Symbol("dialogInjectionKey"),Dg=xe({center:Boolean,alignCenter:Boolean,closeIcon:{type:jt},draggable:Boolean,overflow:Boolean,fullscreen:Boolean,showClose:{type:Boolean,default:!0},title:{type:String,default:""},ariaLevel:{type:String,default:"2"}}),Yx={close:()=>!0},Jx=Z({name:"ElDialogContent"}),Xx=Z({...Jx,props:Dg,emits:Yx,setup(e,{expose:t}){const n=e,{t:r}=sl(),{Close:o}=HT,{dialogRef:s,headerRef:i,bodyId:a,ns:l,style:u}=Ee(Bg),{focusTrapRef:c}=Ee(Cg),f=C(()=>[l.b(),l.is("fullscreen",n.fullscreen),l.is("draggable",n.draggable),l.is("align-center",n.alignCenter),{[l.m("center")]:n.center}]),d=qT(c,s),v=C(()=>n.draggable),p=C(()=>n.overflow),{resetPosition:h}=zm(s,i,v,p);return t({resetPosition:h}),(b,m)=>(L(),ee("div",{ref:g(d),class:U(g(f)),style:ot(g(u)),tabindex:"-1"},[le("header",{ref_key:"headerRef",ref:i,class:U([g(l).e("header"),{"show-close":b.showClose}])},[de(b.$slots,"header",{},()=>[le("span",{role:"heading","aria-level":b.ariaLevel,class:U(g(l).e("title"))},He(b.title),11,["aria-level"])]),b.showClose?(L(),ee("button",{key:0,"aria-label":g(r)("el.dialog.close"),class:U(g(l).e("headerbtn")),type:"button",onClick:S=>b.$emit("close")},[re(g(Je),{class:U(g(l).e("close"))},{default:ce(()=>[(L(),fe(Xe(b.closeIcon||g(o))))]),_:1},8,["class"])],10,["aria-label","onClick"])):ae("v-if",!0)],2),le("div",{id:g(a),class:U(g(l).e("body"))},[de(b.$slots,"default")],10,["id"]),b.$slots.footer?(L(),ee("footer",{key:0,class:U(g(l).e("footer"))},[de(b.$slots,"footer")],2)):ae("v-if",!0)],6))}});var Zx=Ne(Xx,[["__file","dialog-content.vue"]]);const Qx=xe({...Dg,appendToBody:Boolean,appendTo:{type:we([String,Object]),default:"body"},beforeClose:{type:we(Function)},destroyOnClose:Boolean,closeOnClickModal:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},lockScroll:{type:Boolean,default:!0},modal:{type:Boolean,default:!0},openDelay:{type:Number,default:0},closeDelay:{type:Number,default:0},top:{type:String},modelValue:Boolean,modalClass:String,width:{type:[String,Number]},zIndex:{type:Number},trapFocus:Boolean,headerAriaLevel:{type:String,default:"2"}}),e3={open:()=>!0,opened:()=>!0,close:()=>!0,closed:()=>!0,[rt]:e=>Bt(e),openAutoFocus:()=>!0,closeAutoFocus:()=>!0},t3=(e,t)=>{var n;const o=Ge().emit,{nextZIndex:s}=Fc();let i="";const a=pr(),l=pr(),u=V(!1),c=V(!1),f=V(!1),d=V((n=e.zIndex)!=null?n:s());let v,p;const h=ll("namespace",Is),b=C(()=>{const P={},O=`--${h.value}-dialog`;return e.fullscreen||(e.top&&(P[`${O}-margin-top`]=e.top),e.width&&(P[`${O}-width`]=pn(e.width))),P}),m=C(()=>e.alignCenter?{display:"flex"}:{});function S(){o("opened")}function _(){o("closed"),o(rt,!1),e.destroyOnClose&&(f.value=!1)}function w(){o("close")}function y(){p==null||p(),v==null||v(),e.openDelay&&e.openDelay>0?{stop:v}=bu(()=>I(),e.openDelay):I()}function A(){v==null||v(),p==null||p(),e.closeDelay&&e.closeDelay>0?{stop:p}=bu(()=>x(),e.closeDelay):x()}function R(){function P(O){O||(c.value=!0,u.value=!1)}e.beforeClose?e.beforeClose(P):A()}function N(){e.closeOnClickModal&&R()}function I(){st&&(u.value=!0)}function x(){u.value=!1}function k(){o("openAutoFocus")}function $(){o("closeAutoFocus")}function F(P){var O;((O=P.detail)==null?void 0:O.focusReason)==="pointer"&&P.preventDefault()}e.lockScroll&&Km(u);function Y(){e.closeOnPressEscape&&R()}return he(()=>e.modelValue,P=>{P?(c.value=!1,y(),f.value=!0,d.value=Rm(e.zIndex)?s():d.value++,Ie(()=>{o("open"),t.value&&(t.value.scrollTop=0)})):u.value&&A()}),he(()=>e.fullscreen,P=>{t.value&&(P?(i=t.value.style.transform,t.value.style.transform=""):t.value.style.transform=i)}),Ke(()=>{e.modelValue&&(u.value=!0,f.value=!0,y())}),{afterEnter:S,afterLeave:_,beforeLeave:w,handleClose:R,onModalClick:N,close:A,doClose:x,onOpenAutoFocus:k,onCloseAutoFocus:$,onCloseRequested:Y,onFocusoutPrevented:F,titleId:a,bodyId:l,closed:c,style:b,overlayDialogStyle:m,rendered:f,visible:u,zIndex:d}},n3=Z({name:"ElDialog",inheritAttrs:!1}),r3=Z({...n3,props:Qx,emits:e3,setup(e,{expose:t}){const n=e,r=go();xs({scope:"el-dialog",from:"the title slot",replacement:"the header slot",version:"3.0.0",ref:"https://element-plus.org/en-US/component/dialog.html#slots"},C(()=>!!r.title));const o=$e("dialog"),s=V(),i=V(),a=V(),{visible:l,titleId:u,bodyId:c,style:f,overlayDialogStyle:d,rendered:v,zIndex:p,afterEnter:h,afterLeave:b,beforeLeave:m,handleClose:S,onModalClick:_,onOpenAutoFocus:w,onCloseAutoFocus:y,onCloseRequested:A,onFocusoutPrevented:R}=t3(n,s);ft(Bg,{dialogRef:s,headerRef:i,bodyId:c,ns:o,rendered:v,style:f});const N=kc(_),I=C(()=>n.draggable&&!n.fullscreen);return t({visible:l,dialogContentRef:a,resetPosition:()=>{var k;(k=a.value)==null||k.resetPosition()}}),(k,$)=>(L(),fe(g(Ag),{to:k.appendTo,disabled:k.appendTo!=="body"?!1:!k.appendToBody},{default:ce(()=>[re(Fr,{name:"dialog-fade",onAfterEnter:g(h),onAfterLeave:g(b),onBeforeLeave:g(m),persisted:""},{default:ce(()=>[ct(re(g(Fg),{"custom-mask-event":"",mask:k.modal,"overlay-class":k.modalClass,"z-index":g(p)},{default:ce(()=>[le("div",{role:"dialog","aria-modal":"true","aria-label":k.title||void 0,"aria-labelledby":k.title?void 0:g(u),"aria-describedby":g(c),class:U(`${g(o).namespace.value}-overlay-dialog`),style:ot(g(d)),onClick:g(N).onClick,onMousedown:g(N).onMousedown,onMouseup:g(N).onMouseup},[re(g(Hc),{loop:"",trapped:g(l),"focus-start-el":"container",onFocusAfterTrapped:g(w),onFocusAfterReleased:g(y),onFocusoutPrevented:g(R),onReleaseRequested:g(A)},{default:ce(()=>[g(v)?(L(),fe(Zx,En({key:0,ref_key:"dialogContentRef",ref:a},k.$attrs,{center:k.center,"align-center":k.alignCenter,"close-icon":k.closeIcon,draggable:g(I),overflow:k.overflow,fullscreen:k.fullscreen,"show-close":k.showClose,title:k.title,"aria-level":k.headerAriaLevel,onClose:g(S)}),lv({header:ce(()=>[k.$slots.title?de(k.$slots,"title",{key:1}):de(k.$slots,"header",{key:0,close:g(S),titleId:g(u),titleClass:g(o).e("title")})]),default:ce(()=>[de(k.$slots,"default")]),_:2},[k.$slots.footer?{name:"footer",fn:ce(()=>[de(k.$slots,"footer")])}:void 0]),1040,["center","align-center","close-icon","draggable","overflow","fullscreen","show-close","title","aria-level","onClose"])):ae("v-if",!0)]),_:3},8,["trapped","onFocusAfterTrapped","onFocusAfterReleased","onFocusoutPrevented","onReleaseRequested"])],46,["aria-label","aria-labelledby","aria-describedby","onClick","onMousedown","onMouseup"])]),_:3},8,["mask","overlay-class","z-index"]),[[en,g(l)]])]),_:3},8,["onAfterEnter","onAfterLeave","onBeforeLeave"])]),_:3},8,["to","disabled"]))}});var o3=Ne(r3,[["__file","dialog.vue"]]);const MN=yt(o3),s3=Z({inheritAttrs:!1});function i3(e,t,n,r,o,s){return de(e.$slots,"default")}var a3=Ne(s3,[["render",i3],["__file","collection.vue"]]);const l3=Z({name:"ElCollectionItem",inheritAttrs:!1});function u3(e,t,n,r,o,s){return de(e.$slots,"default")}var c3=Ne(l3,[["render",u3],["__file","collection-item.vue"]]);const f3="data-el-collection-item",d3=e=>{const t=`El${e}Collection`,n=`${t}Item`,r=Symbol(t),o=Symbol(n),s={...a3,name:t,setup(){const a=V(null),l=new Map;ft(r,{itemMap:l,getItems:()=>{const c=g(a);if(!c)return[];const f=Array.from(c.querySelectorAll(`[${f3}]`));return[...l.values()].sort((v,p)=>f.indexOf(v.ref)-f.indexOf(p.ref))},collectionRef:a})}},i={...c3,name:n,setup(a,{attrs:l}){const u=V(null),c=Ee(r,void 0);ft(o,{collectionItemRef:u}),Ke(()=>{const f=g(u);f&&c.itemMap.set(f,{ref:f,...l})}),St(()=>{const f=g(u);c.itemMap.delete(f)})}};return{COLLECTION_INJECTION_KEY:r,COLLECTION_ITEM_INJECTION_KEY:o,ElCollection:s,ElCollectionItem:i}},Ul=xe({trigger:ei.trigger,effect:{...Gt.effect,default:"light"},type:{type:we(String)},placement:{type:we(String),default:"bottom"},popperOptions:{type:we(Object),default:()=>({})},id:String,size:{type:String,default:""},splitButton:Boolean,hideOnClick:{type:Boolean,default:!0},loop:{type:Boolean,default:!0},showTimeout:{type:Number,default:150},hideTimeout:{type:Number,default:150},tabindex:{type:we([Number,String]),default:0},maxHeight:{type:we([Number,String]),default:""},popperClass:{type:String,default:""},disabled:Boolean,role:{type:String,default:"menu"},buttonProps:{type:we(Object)},teleported:Gt.teleported});xe({command:{type:[Object,String,Number],default:()=>({})},disabled:Boolean,divided:Boolean,textValue:String,icon:{type:jt}});xe({onKeydown:{type:we(Function)}});d3("Dropdown");const p3=xe({id:{type:String,default:void 0},step:{type:Number,default:1},stepStrictly:Boolean,max:{type:Number,default:Number.POSITIVE_INFINITY},min:{type:Number,default:Number.NEGATIVE_INFINITY},modelValue:Number,readonly:Boolean,disabled:Boolean,size:So,controls:{type:Boolean,default:!0},controlsPosition:{type:String,default:"",values:["","right"]},valueOnClear:{type:[String,Number,null],validator:e=>e===null||je(e)||["min","max"].includes(e),default:null},name:String,placeholder:String,precision:{type:Number,validator:e=>e>=0&&e===Number.parseInt(`${e}`,10)},validateEvent:{type:Boolean,default:!0},...yr(["ariaLabel"])}),h3={[fo]:(e,t)=>t!==e,blur:e=>e instanceof FocusEvent,focus:e=>e instanceof FocusEvent,[io]:e=>je(e)||qn(e),[rt]:e=>je(e)||qn(e)},v3=Z({name:"ElInputNumber"}),m3=Z({...v3,props:p3,emits:h3,setup(e,{expose:t,emit:n}){const r=e,{t:o}=sl(),s=$e("input-number"),i=V(),a=wt({currentValue:r.modelValue,userInput:null}),{formItem:l}=Vr(),u=C(()=>je(r.modelValue)&&r.modelValue<=r.min),c=C(()=>je(r.modelValue)&&r.modelValue>=r.max),f=C(()=>{const P=m(r.step);return Lt(r.precision)?Math.max(m(r.modelValue),P):(P>r.precision,r.precision)}),d=C(()=>r.controls&&r.controlsPosition==="right"),v=In(),p=ns(),h=C(()=>{if(a.userInput!==null)return a.userInput;let P=a.currentValue;if(qn(P))return"";if(je(P)){if(Number.isNaN(P))return"";Lt(r.precision)||(P=P.toFixed(r.precision))}return P}),b=(P,O)=>{if(Lt(O)&&(O=f.value),O===0)return Math.round(P);let j=String(P);const Q=j.indexOf(".");if(Q===-1||!j.replace(".","").split("")[Q+O])return P;const Re=j.length;return j.charAt(Re-1)==="5"&&(j=`${j.slice(0,Math.max(0,Re-1))}6`),Number.parseFloat(Number(j).toFixed(O))},m=P=>{if(qn(P))return 0;const O=P.toString(),j=O.indexOf(".");let Q=0;return j!==-1&&(Q=O.length-j-1),Q},S=(P,O=1)=>je(P)?b(P+r.step*O):a.currentValue,_=()=>{if(r.readonly||p.value||c.value)return;const P=Number(h.value)||0,O=S(P);A(O),n(io,a.currentValue),F()},w=()=>{if(r.readonly||p.value||u.value)return;const P=Number(h.value)||0,O=S(P,-1);A(O),n(io,a.currentValue),F()},y=(P,O)=>{const{max:j,min:Q,step:me,precision:Pe,stepStrictly:Re,valueOnClear:Ce}=r;jj||_ej?j:Q,O&&n(rt,_e)),_e},A=(P,O=!0)=>{var j;const Q=a.currentValue,me=y(P);if(!O){n(rt,me);return}Q===me&&P||(a.userInput=null,n(rt,me),Q!==me&&n(fo,me,Q),r.validateEvent&&((j=l==null?void 0:l.validate)==null||j.call(l,"change").catch(Pe=>void 0)),a.currentValue=me)},R=P=>{a.userInput=P;const O=P===""?null:Number(P);n(io,O),A(O,!1)},N=P=>{const O=P!==""?Number(P):"";(je(O)&&!Number.isNaN(O)||P==="")&&A(O),F(),a.userInput=null},I=()=>{var P,O;(O=(P=i.value)==null?void 0:P.focus)==null||O.call(P)},x=()=>{var P,O;(O=(P=i.value)==null?void 0:P.blur)==null||O.call(P)},k=P=>{n("focus",P)},$=P=>{var O;a.userInput=null,n("blur",P),r.validateEvent&&((O=l==null?void 0:l.validate)==null||O.call(l,"blur").catch(j=>void 0))},F=()=>{a.currentValue!==r.modelValue&&(a.currentValue=r.modelValue)},Y=P=>{document.activeElement===P.target&&P.preventDefault()};return he(()=>r.modelValue,(P,O)=>{const j=y(P,!0);a.userInput===null&&j!==O&&(a.currentValue=j)},{immediate:!0}),Ke(()=>{var P;const{min:O,max:j,modelValue:Q}=r,me=(P=i.value)==null?void 0:P.input;if(me.setAttribute("role","spinbutton"),Number.isFinite(j)?me.setAttribute("aria-valuemax",String(j)):me.removeAttribute("aria-valuemax"),Number.isFinite(O)?me.setAttribute("aria-valuemin",String(O)):me.removeAttribute("aria-valuemin"),me.setAttribute("aria-valuenow",a.currentValue||a.currentValue===0?String(a.currentValue):""),me.setAttribute("aria-disabled",String(p.value)),!je(Q)&&Q!=null){let Pe=Number(Q);Number.isNaN(Pe)&&(Pe=null),n(rt,Pe)}me.addEventListener("wheel",Y,{passive:!1})}),mo(()=>{var P,O;const j=(P=i.value)==null?void 0:P.input;j==null||j.setAttribute("aria-valuenow",`${(O=a.currentValue)!=null?O:""}`)}),t({focus:I,blur:x}),(P,O)=>(L(),ee("div",{class:U([g(s).b(),g(s).m(g(v)),g(s).is("disabled",g(p)),g(s).is("without-controls",!P.controls),g(s).is("controls-right",g(d))]),onDragstart:tt(()=>{},["prevent"])},[P.controls?ct((L(),ee("span",{key:0,role:"button","aria-label":g(o)("el.inputNumber.decrease"),class:U([g(s).e("decrease"),g(s).is("disabled",g(u))]),onKeydown:Vt(w,["enter"])},[de(P.$slots,"decrease-icon",{},()=>[re(g(Je),null,{default:ce(()=>[g(d)?(L(),fe(g(Nm),{key:0})):(L(),fe(g($T),{key:1}))]),_:1})])],42,["aria-label","onKeydown"])),[[g(kp),w]]):ae("v-if",!0),P.controls?ct((L(),ee("span",{key:1,role:"button","aria-label":g(o)("el.inputNumber.increase"),class:U([g(s).e("increase"),g(s).is("disabled",g(c))]),onKeydown:Vt(_,["enter"])},[de(P.$slots,"increase-icon",{},()=>[re(g(Je),null,{default:ce(()=>[g(d)?(L(),fe(g(bT),{key:0})):(L(),fe(g($m),{key:1}))]),_:1})])],42,["aria-label","onKeydown"])),[[g(kp),_]]):ae("v-if",!0),re(g(bg),{id:P.id,ref_key:"input",ref:i,type:"number",step:P.step,"model-value":g(h),placeholder:P.placeholder,readonly:P.readonly,disabled:g(p),size:g(v),max:P.max,min:P.min,name:P.name,"aria-label":P.ariaLabel,"validate-event":!1,onKeydown:[Vt(tt(_,["prevent"]),["up"]),Vt(tt(w,["prevent"]),["down"])],onBlur:$,onFocus:k,onInput:R,onChange:N},lv({_:2},[P.$slots.prefix?{name:"prefix",fn:ce(()=>[de(P.$slots,"prefix")])}:void 0,P.$slots.suffix?{name:"suffix",fn:ce(()=>[de(P.$slots,"suffix")])}:void 0]),1032,["id","step","model-value","placeholder","readonly","disabled","size","max","min","name","aria-label","onKeydown"])],42,["onDragstart"]))}});var g3=Ne(m3,[["__file","input-number.vue"]]);const $N=yt(g3),b3=xe({type:{type:String,values:["primary","success","warning","info","danger","default"],default:"default"},underline:{type:Boolean,default:!0},disabled:Boolean,href:{type:String,default:""},target:{type:String,default:"_self"},icon:{type:jt}}),y3={click:e=>e instanceof MouseEvent},w3=Z({name:"ElLink"}),S3=Z({...w3,props:b3,emits:y3,setup(e,{emit:t}){const n=e,r=$e("link"),o=C(()=>[r.b(),r.m(n.type),r.is("disabled",n.disabled),r.is("underline",n.underline&&!n.disabled)]);function s(i){n.disabled||t("click",i)}return(i,a)=>(L(),ee("a",{class:U(g(o)),href:i.disabled||!i.href?void 0:i.href,target:i.disabled||!i.href?void 0:i.target,onClick:s},[i.icon?(L(),fe(g(Je),{key:0},{default:ce(()=>[(L(),fe(Xe(i.icon)))]),_:1})):ae("v-if",!0),i.$slots.default?(L(),ee("span",{key:1,class:U(g(r).e("inner"))},[de(i.$slots,"default")],2)):ae("v-if",!0),i.$slots.icon?de(i.$slots,"icon",{key:2}):ae("v-if",!0)],10,["href","target"]))}});var E3=Ne(S3,[["__file","link.vue"]]);const kN=yt(E3),Vg=Symbol("ElSelectGroup"),cl=Symbol("ElSelect");function _3(e,t){const n=Ee(cl),r=Ee(Vg,{disabled:!1}),o=C(()=>c(yn(n.props.modelValue),e.value)),s=C(()=>{var v;if(n.props.multiple){const p=yn((v=n.props.modelValue)!=null?v:[]);return!o.value&&p.length>=n.props.multipleLimit&&n.props.multipleLimit>0}else return!1}),i=C(()=>e.label||(Oe(e.value)?"":e.value)),a=C(()=>e.value||e.label||""),l=C(()=>e.disabled||t.groupDisabled||s.value),u=Ge(),c=(v=[],p)=>{if(Oe(e.value)){const h=n.props.valueKey;return v&&v.some(b=>Me(zn(b,h))===zn(p,h))}else return v&&v.includes(p)},f=()=>{!e.disabled&&!r.disabled&&(n.states.hoveringIndex=n.optionsArray.indexOf(u.proxy))},d=v=>{const p=new RegExp(lT(v),"i");t.visible=p.test(i.value)||e.created};return he(()=>i.value,()=>{!e.created&&!n.props.remote&&n.setSelected()}),he(()=>e.value,(v,p)=>{const{remote:h,valueKey:b}=n.props;if(v!==p&&(n.onOptionDestroy(p,u.proxy),n.onOptionCreate(u.proxy)),!e.created&&!h){if(b&&Oe(v)&&Oe(p)&&v[b]===p[b])return;n.setSelected()}}),he(()=>r.disabled,()=>{t.groupDisabled=r.disabled},{immediate:!0}),{select:n,currentLabel:i,currentValue:a,itemSelected:o,isDisabled:l,hoverItem:f,updateOption:d}}const C3=Z({name:"ElOption",componentName:"ElOption",props:{value:{required:!0,type:[String,Number,Boolean,Object]},label:[String,Number],created:Boolean,disabled:Boolean},setup(e){const t=$e("select"),n=pr(),r=C(()=>[t.be("dropdown","item"),t.is("disabled",g(a)),t.is("selected",g(i)),t.is("hovering",g(d))]),o=wt({index:-1,groupDisabled:!1,visible:!0,hover:!1}),{currentLabel:s,itemSelected:i,isDisabled:a,select:l,hoverItem:u,updateOption:c}=_3(e,o),{visible:f,hover:d}=vr(o),v=Ge().proxy;l.onOptionCreate(v),St(()=>{const h=v.value,{selected:b}=l.states,S=(l.props.multiple?b:[b]).some(_=>_.value===v.value);Ie(()=>{l.states.cachedOptions.get(h)===v&&!S&&l.states.cachedOptions.delete(h)}),l.onOptionDestroy(h,v)});function p(){a.value||l.handleOptionSelect(v)}return{ns:t,id:n,containerKls:r,currentLabel:s,itemSelected:i,isDisabled:a,select:l,hoverItem:u,updateOption:c,visible:f,hover:d,selectOptionClick:p,states:o}}});function T3(e,t,n,r,o,s){return ct((L(),ee("li",{id:e.id,class:U(e.containerKls),role:"option","aria-disabled":e.isDisabled||void 0,"aria-selected":e.itemSelected,onMouseenter:e.hoverItem,onClick:tt(e.selectOptionClick,["stop"])},[de(e.$slots,"default",{},()=>[le("span",null,He(e.currentLabel),1)])],42,["id","aria-disabled","aria-selected","onMouseenter","onClick"])),[[en,e.visible]])}var Kc=Ne(C3,[["render",T3],["__file","option.vue"]]);const O3=Z({name:"ElSelectDropdown",componentName:"ElSelectDropdown",setup(){const e=Ee(cl),t=$e("select"),n=C(()=>e.props.popperClass),r=C(()=>e.props.multiple),o=C(()=>e.props.fitInputWidth),s=V("");function i(){var a;s.value=`${(a=e.selectRef)==null?void 0:a.offsetWidth}px`}return Ke(()=>{i(),Dt(e.selectRef,i)}),{ns:t,minWidth:s,popperClass:n,isMultiple:r,isFitInputWidth:o}}});function A3(e,t,n,r,o,s){return L(),ee("div",{class:U([e.ns.b("dropdown"),e.ns.is("multiple",e.isMultiple),e.popperClass]),style:ot({[e.isFitInputWidth?"width":"minWidth"]:e.minWidth})},[e.$slots.header?(L(),ee("div",{key:0,class:U(e.ns.be("dropdown","header"))},[de(e.$slots,"header")],2)):ae("v-if",!0),de(e.$slots,"default"),e.$slots.footer?(L(),ee("div",{key:1,class:U(e.ns.be("dropdown","footer"))},[de(e.$slots,"footer")],2)):ae("v-if",!0)],6)}var R3=Ne(O3,[["render",A3],["__file","select-dropdown.vue"]]);const x3=11,I3=(e,t)=>{const{t:n}=sl(),r=pr(),o=$e("select"),s=$e("input"),i=wt({inputValue:"",options:new Map,cachedOptions:new Map,disabledOptions:new Map,optionValues:[],selected:[],selectionWidth:0,calculatorWidth:0,collapseItemWidth:0,selectedLabel:"",hoveringIndex:-1,previousQuery:null,inputHovering:!1,menuVisibleOnFocus:!1,isBeforeHide:!1}),a=V(null),l=V(null),u=V(null),c=V(null),f=V(null),d=V(null),v=V(null),p=V(null),h=V(null),b=V(null),m=V(null),S=V(null),{isComposing:_,handleCompositionStart:w,handleCompositionUpdate:y,handleCompositionEnd:A}=dg({afterComposition:H=>ke(H)}),{wrapperRef:R,isFocused:N,handleBlur:I}=fg(f,{beforeFocus(){return j.value},afterFocus(){e.automaticDropdown&&!x.value&&(x.value=!0,i.menuVisibleOnFocus=!0)},beforeBlur(H){var ie,Fe;return((ie=u.value)==null?void 0:ie.isFocusInsideContent(H))||((Fe=c.value)==null?void 0:Fe.isFocusInsideContent(H))},afterBlur(){x.value=!1,i.menuVisibleOnFocus=!1}}),x=V(!1),k=V(),{form:$,formItem:F}=Vr(),{inputId:Y}=hi(e,{formItemContext:F}),{valueOnClear:P,isEmptyValue:O}=_A(e),j=C(()=>e.disabled||($==null?void 0:$.disabled)),Q=C(()=>pe(e.modelValue)?e.modelValue.length>0:!O(e.modelValue)),me=C(()=>e.clearable&&!j.value&&i.inputHovering&&Q.value),Pe=C(()=>e.remote&&e.filterable&&!e.remoteShowSuffix?"":e.suffixIcon),Re=C(()=>o.is("reverse",Pe.value&&x.value)),Ce=C(()=>(F==null?void 0:F.validateState)||""),_e=C(()=>Vm[Ce.value]),qe=C(()=>e.remote?300:0),ze=C(()=>e.loading?e.loadingText||n("el.select.loading"):e.remote&&!i.inputValue&&i.options.size===0?!1:e.filterable&&i.inputValue&&i.options.size>0&&De.value===0?e.noMatchText||n("el.select.noMatch"):i.options.size===0?e.noDataText||n("el.select.noData"):null),De=C(()=>B.value.filter(H=>H.visible).length),B=C(()=>{const H=Array.from(i.options.values()),ie=[];return i.optionValues.forEach(Fe=>{const Ze=H.findIndex(wr=>wr.value===Fe);Ze>-1&&ie.push(H[Ze])}),ie.length>=H.length?ie:H}),K=C(()=>Array.from(i.cachedOptions.values())),J=C(()=>{const H=B.value.filter(ie=>!ie.created).some(ie=>ie.currentLabel===i.inputValue);return e.filterable&&e.allowCreate&&i.inputValue!==""&&!H}),oe=()=>{e.filterable&&ve(e.filterMethod)||e.filterable&&e.remote&&ve(e.remoteMethod)||B.value.forEach(H=>{var ie;(ie=H.updateOption)==null||ie.call(H,i.inputValue)})},ge=In(),E=C(()=>["small"].includes(ge.value)?"small":"default"),T=C({get(){return x.value&&ze.value!==!1},set(H){x.value=H}}),M=C(()=>{if(e.multiple&&!Lt(e.modelValue))return yn(e.modelValue).length===0&&!i.inputValue;const H=pe(e.modelValue)?e.modelValue[0]:e.modelValue;return e.filterable||Lt(H)?!i.inputValue:!0}),W=C(()=>{var H;const ie=(H=e.placeholder)!=null?H:n("el.select.placeholder");return e.multiple||!Q.value?ie:i.selectedLabel}),G=C(()=>gu?null:"mouseenter");he(()=>e.modelValue,(H,ie)=>{e.multiple&&e.filterable&&!e.reserveKeyword&&(i.inputValue="",q("")),ne(),!Aa(H,ie)&&e.validateEvent&&(F==null||F.validate("change").catch(Fe=>void 0))},{flush:"post",deep:!0}),he(()=>x.value,H=>{H?q(i.inputValue):(i.inputValue="",i.previousQuery=null,i.isBeforeHide=!0),t("visible-change",H)}),he(()=>i.options.entries(),()=>{var H;if(!st)return;const ie=((H=a.value)==null?void 0:H.querySelectorAll("input"))||[];(!e.filterable&&!e.defaultFirstOption&&!Lt(e.modelValue)||!Array.from(ie).includes(document.activeElement))&&ne(),e.defaultFirstOption&&(e.filterable||e.remote)&&De.value&&se()},{flush:"post"}),he(()=>i.hoveringIndex,H=>{je(H)&&H>-1?k.value=B.value[H]||{}:k.value={},B.value.forEach(ie=>{ie.hover=k.value===ie})}),Ha(()=>{i.isBeforeHide||oe()});const q=H=>{i.previousQuery===H||_.value||(i.previousQuery=H,e.filterable&&ve(e.filterMethod)?e.filterMethod(H):e.filterable&&e.remote&&ve(e.remoteMethod)&&e.remoteMethod(H),e.defaultFirstOption&&(e.filterable||e.remote)&&De.value?Ie(se):Ie(X))},se=()=>{const H=B.value.filter(Ze=>Ze.visible&&!Ze.disabled&&!Ze.states.groupDisabled),ie=H.find(Ze=>Ze.created),Fe=H[0];i.hoveringIndex=qt(B.value,ie||Fe)},ne=()=>{if(e.multiple)i.selectedLabel="";else{const ie=pe(e.modelValue)?e.modelValue[0]:e.modelValue,Fe=te(ie);i.selectedLabel=Fe.currentLabel,i.selected=[Fe];return}const H=[];Lt(e.modelValue)||yn(e.modelValue).forEach(ie=>{H.push(te(ie))}),i.selected=H},te=H=>{let ie;const Fe=Ui(H).toLowerCase()==="object",Ze=Ui(H).toLowerCase()==="null",wr=Ui(H).toLowerCase()==="undefined";for(let Hr=i.cachedOptions.size-1;Hr>=0;Hr--){const Ln=K.value[Hr];if(Fe?zn(Ln.value,e.valueKey)===zn(H,e.valueKey):Ln.value===H){ie={value:H,currentLabel:Ln.currentLabel,get isDisabled(){return Ln.isDisabled}};break}}if(ie)return ie;const Eo=Fe?H.label:!Ze&&!wr?H:"";return{value:H,currentLabel:Eo}},X=()=>{i.hoveringIndex=B.value.findIndex(H=>i.selected.some(ie=>bl(ie)===bl(H)))},Se=()=>{i.selectionWidth=l.value.getBoundingClientRect().width},ue=()=>{i.calculatorWidth=d.value.getBoundingClientRect().width},ye=()=>{i.collapseItemWidth=m.value.getBoundingClientRect().width},z=()=>{var H,ie;(ie=(H=u.value)==null?void 0:H.updatePopper)==null||ie.call(H)},be=()=>{var H,ie;(ie=(H=c.value)==null?void 0:H.updatePopper)==null||ie.call(H)},Le=()=>{i.inputValue.length>0&&!x.value&&(x.value=!0),q(i.inputValue)},ke=H=>{if(i.inputValue=H.target.value,e.remote)dt();else return Le()},dt=nT(()=>{Le()},qe.value),pt=H=>{Aa(e.modelValue,H)||t(fo,H)},rn=H=>rT(H,ie=>!i.disabledOptions.has(ie)),on=H=>{if(e.multiple&&H.code!==_n.delete&&H.target.value.length<=0){const ie=yn(e.modelValue).slice(),Fe=rn(ie);if(Fe<0)return;const Ze=ie[Fe];ie.splice(Fe,1),t(rt,ie),pt(ie),t("remove-tag",Ze)}},jr=(H,ie)=>{const Fe=i.selected.indexOf(ie);if(Fe>-1&&!j.value){const Ze=yn(e.modelValue).slice();Ze.splice(Fe,1),t(rt,Ze),pt(Ze),t("remove-tag",ie.value)}H.stopPropagation(),Ei()},as=H=>{H.stopPropagation();const ie=e.multiple?[]:P.value;if(e.multiple)for(const Fe of i.selected)Fe.isDisabled&&ie.push(Fe.value);t(rt,ie),pt(ie),i.hoveringIndex=-1,x.value=!1,t("clear"),Ei()},Ot=H=>{var ie;if(e.multiple){const Fe=yn((ie=e.modelValue)!=null?ie:[]).slice(),Ze=qt(Fe,H.value);Ze>-1?Fe.splice(Ze,1):(e.multipleLimit<=0||Fe.length{zr(H)})},qt=(H=[],ie)=>{if(!Oe(ie))return H.indexOf(ie);const Fe=e.valueKey;let Ze=-1;return H.some((wr,Eo)=>Me(zn(wr,Fe))===zn(ie,Fe)?(Ze=Eo,!0):!1),Ze},zr=H=>{var ie,Fe,Ze,wr,Eo;const _i=pe(H)?H[0]:H;let Hr=null;if(_i!=null&&_i.value){const Ln=B.value.filter(tf=>tf.value===_i.value);Ln.length>0&&(Hr=Ln[0].$el)}if(u.value&&Hr){const Ln=(wr=(Ze=(Fe=(ie=u.value)==null?void 0:ie.popperRef)==null?void 0:Fe.contentRef)==null?void 0:Ze.querySelector)==null?void 0:wr.call(Ze,`.${o.be("dropdown","wrap")}`);Ln&&fT(Ln,Hr)}(Eo=S.value)==null||Eo.handleScroll()},Si=H=>{i.options.set(H.value,H),i.cachedOptions.set(H.value,H),H.disabled&&i.disabledOptions.set(H.value,H)},Nb=(H,ie)=>{i.options.get(H)===ie&&i.options.delete(H)},Lb=C(()=>{var H,ie;return(ie=(H=u.value)==null?void 0:H.popperRef)==null?void 0:ie.contentRef}),Mb=()=>{i.isBeforeHide=!1,Ie(()=>zr(i.selected))},Ei=()=>{var H;(H=f.value)==null||H.focus()},$b=()=>{var H;if(x.value){x.value=!1,Ie(()=>{var ie;return(ie=f.value)==null?void 0:ie.blur()});return}(H=f.value)==null||H.blur()},kb=H=>{as(H)},Fb=H=>{if(x.value=!1,N.value){const ie=new FocusEvent("focus",H);Ie(()=>I(ie))}},Bb=()=>{i.inputValue.length>0?i.inputValue="":x.value=!1},Qc=()=>{j.value||(gu&&(i.inputHovering=!0),i.menuVisibleOnFocus?i.menuVisibleOnFocus=!1:x.value=!x.value)},Db=()=>{x.value?B.value[i.hoveringIndex]&&Ot(B.value[i.hoveringIndex]):Qc()},bl=H=>Oe(H.value)?zn(H.value,e.valueKey):H.value,Vb=C(()=>B.value.filter(H=>H.visible).every(H=>H.disabled)),jb=C(()=>e.multiple?e.collapseTags?i.selected.slice(0,e.maxCollapseTags):i.selected:[]),zb=C(()=>e.multiple?e.collapseTags?i.selected.slice(e.maxCollapseTags):[]:[]),ef=H=>{if(!x.value){x.value=!0;return}if(!(i.options.size===0||i.filteredOptionsCount===0||_.value)&&!Vb.value){H==="next"?(i.hoveringIndex++,i.hoveringIndex===i.options.size&&(i.hoveringIndex=0)):H==="prev"&&(i.hoveringIndex--,i.hoveringIndex<0&&(i.hoveringIndex=i.options.size-1));const ie=B.value[i.hoveringIndex];(ie.disabled===!0||ie.states.groupDisabled===!0||!ie.visible)&&ef(H),Ie(()=>zr(k.value))}},Hb=()=>{if(!l.value)return 0;const H=window.getComputedStyle(l.value);return Number.parseFloat(H.gap||"6px")},Ub=C(()=>{const H=Hb();return{maxWidth:`${m.value&&e.maxCollapseTags===1?i.selectionWidth-i.collapseItemWidth-H:i.selectionWidth}px`}}),Kb=C(()=>({maxWidth:`${i.selectionWidth}px`})),qb=C(()=>({width:`${Math.max(i.calculatorWidth,x3)}px`}));return Dt(l,Se),Dt(d,ue),Dt(h,z),Dt(R,z),Dt(b,be),Dt(m,ye),Ke(()=>{ne()}),{inputId:Y,contentId:r,nsSelect:o,nsInput:s,states:i,isFocused:N,expanded:x,optionsArray:B,hoverOption:k,selectSize:ge,filteredOptionsCount:De,resetCalculatorWidth:ue,updateTooltip:z,updateTagTooltip:be,debouncedOnInputChange:dt,onInput:ke,deletePrevTag:on,deleteTag:jr,deleteSelected:as,handleOptionSelect:Ot,scrollToOption:zr,hasModelValue:Q,shouldShowPlaceholder:M,currentPlaceholder:W,mouseEnterEventName:G,showClose:me,iconComponent:Pe,iconReverse:Re,validateState:Ce,validateIcon:_e,showNewOption:J,updateOptions:oe,collapseTagSize:E,setSelected:ne,selectDisabled:j,emptyText:ze,handleCompositionStart:w,handleCompositionUpdate:y,handleCompositionEnd:A,onOptionCreate:Si,onOptionDestroy:Nb,handleMenuEnter:Mb,focus:Ei,blur:$b,handleClearClick:kb,handleClickOutside:Fb,handleEsc:Bb,toggleMenu:Qc,selectOption:Db,getValueKey:bl,navigateOptions:ef,dropdownMenuVisible:T,showTagList:jb,collapseTagList:zb,tagStyle:Ub,collapseTagStyle:Kb,inputStyle:qb,popperRef:Lb,inputRef:f,tooltipRef:u,tagTooltipRef:c,calculatorRef:d,prefixRef:v,suffixRef:p,selectRef:a,wrapperRef:R,selectionRef:l,scrollbarRef:S,menuRef:h,tagMenuRef:b,collapseItemRef:m}};var P3=Z({name:"ElOptions",setup(e,{slots:t}){const n=Ee(cl);let r=[];return()=>{var o,s;const i=(o=t.default)==null?void 0:o.call(t),a=[];function l(u){pe(u)&&u.forEach(c=>{var f,d,v,p;const h=(f=(c==null?void 0:c.type)||{})==null?void 0:f.name;h==="ElOptionGroup"?l(!Ae(c.children)&&!pe(c.children)&&ve((d=c.children)==null?void 0:d.default)?(v=c.children)==null?void 0:v.default():c.children):h==="ElOption"?a.push((p=c.props)==null?void 0:p.value):pe(c.children)&&l(c.children)})}return i.length&&l((s=i[0])==null?void 0:s.children),Aa(a,r)||(r=a,n&&(n.states.optionValues=a)),i}}});const N3=xe({name:String,id:String,modelValue:{type:[Array,String,Number,Boolean,Object],default:void 0},autocomplete:{type:String,default:"off"},automaticDropdown:Boolean,size:So,effect:{type:we(String),default:"light"},disabled:Boolean,clearable:Boolean,filterable:Boolean,allowCreate:Boolean,loading:Boolean,popperClass:{type:String,default:""},popperOptions:{type:we(Object),default:()=>({})},remote:Boolean,loadingText:String,noMatchText:String,noDataText:String,remoteMethod:Function,filterMethod:Function,multiple:Boolean,multipleLimit:{type:Number,default:0},placeholder:{type:String},defaultFirstOption:Boolean,reserveKeyword:{type:Boolean,default:!0},valueKey:{type:String,default:"value"},collapseTags:Boolean,collapseTagsTooltip:Boolean,maxCollapseTags:{type:Number,default:1},teleported:Gt.teleported,persistent:{type:Boolean,default:!0},clearIcon:{type:jt,default:Oc},fitInputWidth:Boolean,suffixIcon:{type:jt,default:Nm},tagType:{...ku.type,default:"info"},tagEffect:{...ku.effect,default:"light"},validateEvent:{type:Boolean,default:!0},remoteShowSuffix:Boolean,placement:{type:we(String),values:il,default:"bottom-start"},fallbackPlacements:{type:we(Array),default:["bottom-start","top-start","right","left"]},appendTo:String,...hg,...yr(["ariaLabel"])}),Bp="ElSelect",L3=Z({name:Bp,componentName:Bp,components:{ElSelectMenu:R3,ElOption:Kc,ElOptions:P3,ElTag:Ux,ElScrollbar:U4,ElTooltip:Rg,ElIcon:Je},directives:{ClickOutside:_x},props:N3,emits:[rt,fo,"remove-tag","clear","visible-change","focus","blur"],setup(e,{emit:t}){const n=C(()=>{const{modelValue:i,multiple:a}=e,l=a?[]:void 0;return pe(i)?a?i:l:a?l:i}),r=wt({...vr(e),modelValue:n}),o=I3(r,t);ft(cl,wt({props:r,states:o.states,optionsArray:o.optionsArray,handleOptionSelect:o.handleOptionSelect,onOptionCreate:o.onOptionCreate,onOptionDestroy:o.onOptionDestroy,selectRef:o.selectRef,setSelected:o.setSelected}));const s=C(()=>e.multiple?o.states.selected.map(i=>i.currentLabel):o.states.selectedLabel);return{...o,modelValue:n,selectedLabel:s}}});function M3(e,t,n,r,o,s){const i=Yt("el-tag"),a=Yt("el-tooltip"),l=Yt("el-icon"),u=Yt("el-option"),c=Yt("el-options"),f=Yt("el-scrollbar"),d=Yt("el-select-menu"),v=Jy("click-outside");return ct((L(),ee("div",{ref:"selectRef",class:U([e.nsSelect.b(),e.nsSelect.m(e.selectSize)]),[Ki(e.mouseEnterEventName)]:p=>e.states.inputHovering=!0,onMouseleave:p=>e.states.inputHovering=!1},[re(a,{ref:"tooltipRef",visible:e.dropdownMenuVisible,placement:e.placement,teleported:e.teleported,"popper-class":[e.nsSelect.e("popper"),e.popperClass],"popper-options":e.popperOptions,"fallback-placements":e.fallbackPlacements,effect:e.effect,pure:"",trigger:"click",transition:`${e.nsSelect.namespace.value}-zoom-in-top`,"stop-popper-mouse-event":!1,"gpu-acceleration":!1,persistent:e.persistent,"append-to":e.appendTo,onBeforeShow:e.handleMenuEnter,onHide:p=>e.states.isBeforeHide=!1},{default:ce(()=>{var p;return[le("div",{ref:"wrapperRef",class:U([e.nsSelect.e("wrapper"),e.nsSelect.is("focused",e.isFocused),e.nsSelect.is("hovering",e.states.inputHovering),e.nsSelect.is("filterable",e.filterable),e.nsSelect.is("disabled",e.selectDisabled)]),onClick:tt(e.toggleMenu,["prevent"])},[e.$slots.prefix?(L(),ee("div",{key:0,ref:"prefixRef",class:U(e.nsSelect.e("prefix"))},[de(e.$slots,"prefix")],2)):ae("v-if",!0),le("div",{ref:"selectionRef",class:U([e.nsSelect.e("selection"),e.nsSelect.is("near",e.multiple&&!e.$slots.prefix&&!!e.states.selected.length)])},[e.multiple?de(e.$slots,"tag",{key:0},()=>[(L(!0),ee(nt,null,vf(e.showTagList,h=>(L(),ee("div",{key:e.getValueKey(h),class:U(e.nsSelect.e("selected-item"))},[re(i,{closable:!e.selectDisabled&&!h.isDisabled,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",style:ot(e.tagStyle),onClose:b=>e.deleteTag(b,h)},{default:ce(()=>[le("span",{class:U(e.nsSelect.e("tags-text"))},[de(e.$slots,"label",{label:h.currentLabel,value:h.value},()=>[Un(He(h.currentLabel),1)])],2)]),_:2},1032,["closable","size","type","effect","style","onClose"])],2))),128)),e.collapseTags&&e.states.selected.length>e.maxCollapseTags?(L(),fe(a,{key:0,ref:"tagTooltipRef",disabled:e.dropdownMenuVisible||!e.collapseTagsTooltip,"fallback-placements":["bottom","top","right","left"],effect:e.effect,placement:"bottom",teleported:e.teleported},{default:ce(()=>[le("div",{ref:"collapseItemRef",class:U(e.nsSelect.e("selected-item"))},[re(i,{closable:!1,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",style:ot(e.collapseTagStyle)},{default:ce(()=>[le("span",{class:U(e.nsSelect.e("tags-text"))}," + "+He(e.states.selected.length-e.maxCollapseTags),3)]),_:1},8,["size","type","effect","style"])],2)]),content:ce(()=>[le("div",{ref:"tagMenuRef",class:U(e.nsSelect.e("selection"))},[(L(!0),ee(nt,null,vf(e.collapseTagList,h=>(L(),ee("div",{key:e.getValueKey(h),class:U(e.nsSelect.e("selected-item"))},[re(i,{class:"in-tooltip",closable:!e.selectDisabled&&!h.isDisabled,size:e.collapseTagSize,type:e.tagType,effect:e.tagEffect,"disable-transitions":"",onClose:b=>e.deleteTag(b,h)},{default:ce(()=>[le("span",{class:U(e.nsSelect.e("tags-text"))},[de(e.$slots,"label",{label:h.currentLabel,value:h.value},()=>[Un(He(h.currentLabel),1)])],2)]),_:2},1032,["closable","size","type","effect","onClose"])],2))),128))],2)]),_:3},8,["disabled","effect","teleported"])):ae("v-if",!0)]):ae("v-if",!0),e.selectDisabled?ae("v-if",!0):(L(),ee("div",{key:1,class:U([e.nsSelect.e("selected-item"),e.nsSelect.e("input-wrapper"),e.nsSelect.is("hidden",!e.filterable)])},[ct(le("input",{id:e.inputId,ref:"inputRef","onUpdate:modelValue":h=>e.states.inputValue=h,type:"text",name:e.name,class:U([e.nsSelect.e("input"),e.nsSelect.is(e.selectSize)]),disabled:e.selectDisabled,autocomplete:e.autocomplete,style:ot(e.inputStyle),role:"combobox",readonly:!e.filterable,spellcheck:"false","aria-activedescendant":((p=e.hoverOption)==null?void 0:p.id)||"","aria-controls":e.contentId,"aria-expanded":e.dropdownMenuVisible,"aria-label":e.ariaLabel,"aria-autocomplete":"none","aria-haspopup":"listbox",onKeydown:[Vt(tt(h=>e.navigateOptions("next"),["stop","prevent"]),["down"]),Vt(tt(h=>e.navigateOptions("prev"),["stop","prevent"]),["up"]),Vt(tt(e.handleEsc,["stop","prevent"]),["esc"]),Vt(tt(e.selectOption,["stop","prevent"]),["enter"]),Vt(tt(e.deletePrevTag,["stop"]),["delete"])],onCompositionstart:e.handleCompositionStart,onCompositionupdate:e.handleCompositionUpdate,onCompositionend:e.handleCompositionEnd,onInput:e.onInput,onClick:tt(e.toggleMenu,["stop"])},null,46,["id","onUpdate:modelValue","name","disabled","autocomplete","readonly","aria-activedescendant","aria-controls","aria-expanded","aria-label","onKeydown","onCompositionstart","onCompositionupdate","onCompositionend","onInput","onClick"]),[[fw,e.states.inputValue]]),e.filterable?(L(),ee("span",{key:0,ref:"calculatorRef","aria-hidden":"true",class:U(e.nsSelect.e("input-calculator")),textContent:He(e.states.inputValue)},null,10,["textContent"])):ae("v-if",!0)],2)),e.shouldShowPlaceholder?(L(),ee("div",{key:2,class:U([e.nsSelect.e("selected-item"),e.nsSelect.e("placeholder"),e.nsSelect.is("transparent",!e.hasModelValue||e.expanded&&!e.states.inputValue)])},[e.hasModelValue?de(e.$slots,"label",{key:0,label:e.currentPlaceholder,value:e.modelValue},()=>[le("span",null,He(e.currentPlaceholder),1)]):(L(),ee("span",{key:1},He(e.currentPlaceholder),1))],2)):ae("v-if",!0)],2),le("div",{ref:"suffixRef",class:U(e.nsSelect.e("suffix"))},[e.iconComponent&&!e.showClose?(L(),fe(l,{key:0,class:U([e.nsSelect.e("caret"),e.nsSelect.e("icon"),e.iconReverse])},{default:ce(()=>[(L(),fe(Xe(e.iconComponent)))]),_:1},8,["class"])):ae("v-if",!0),e.showClose&&e.clearIcon?(L(),fe(l,{key:1,class:U([e.nsSelect.e("caret"),e.nsSelect.e("icon"),e.nsSelect.e("clear")]),onClick:e.handleClearClick},{default:ce(()=>[(L(),fe(Xe(e.clearIcon)))]),_:1},8,["class","onClick"])):ae("v-if",!0),e.validateState&&e.validateIcon?(L(),fe(l,{key:2,class:U([e.nsInput.e("icon"),e.nsInput.e("validateIcon")])},{default:ce(()=>[(L(),fe(Xe(e.validateIcon)))]),_:1},8,["class"])):ae("v-if",!0)],2)],10,["onClick"])]}),content:ce(()=>[re(d,{ref:"menuRef"},{default:ce(()=>[e.$slots.header?(L(),ee("div",{key:0,class:U(e.nsSelect.be("dropdown","header")),onClick:tt(()=>{},["stop"])},[de(e.$slots,"header")],10,["onClick"])):ae("v-if",!0),ct(re(f,{id:e.contentId,ref:"scrollbarRef",tag:"ul","wrap-class":e.nsSelect.be("dropdown","wrap"),"view-class":e.nsSelect.be("dropdown","list"),class:U([e.nsSelect.is("empty",e.filteredOptionsCount===0)]),role:"listbox","aria-label":e.ariaLabel,"aria-orientation":"vertical"},{default:ce(()=>[e.showNewOption?(L(),fe(u,{key:0,value:e.states.inputValue,created:!0},null,8,["value"])):ae("v-if",!0),re(c,null,{default:ce(()=>[de(e.$slots,"default")]),_:3})]),_:3},8,["id","wrap-class","view-class","class","aria-label"]),[[en,e.states.options.size>0&&!e.loading]]),e.$slots.loading&&e.loading?(L(),ee("div",{key:1,class:U(e.nsSelect.be("dropdown","loading"))},[de(e.$slots,"loading")],2)):e.loading||e.filteredOptionsCount===0?(L(),ee("div",{key:2,class:U(e.nsSelect.be("dropdown","empty"))},[de(e.$slots,"empty",{},()=>[le("span",null,He(e.emptyText),1)])],2)):ae("v-if",!0),e.$slots.footer?(L(),ee("div",{key:3,class:U(e.nsSelect.be("dropdown","footer")),onClick:tt(()=>{},["stop"])},[de(e.$slots,"footer")],10,["onClick"])):ae("v-if",!0)]),_:3},512)]),_:3},8,["visible","placement","teleported","popper-class","popper-options","fallback-placements","effect","transition","persistent","append-to","onBeforeShow","onHide"])],16,["onMouseleave"])),[[v,e.handleClickOutside,e.popperRef]])}var $3=Ne(L3,[["render",M3],["__file","select.vue"]]);const k3=Z({name:"ElOptionGroup",componentName:"ElOptionGroup",props:{label:String,disabled:Boolean},setup(e){const t=$e("select"),n=V(null),r=Ge(),o=V([]);ft(Vg,wt({...vr(e)}));const s=C(()=>o.value.some(u=>u.visible===!0)),i=u=>{var c,f;return((c=u.type)==null?void 0:c.name)==="ElOption"&&!!((f=u.component)!=null&&f.proxy)},a=u=>{const c=yn(u),f=[];return c.forEach(d=>{var v,p;i(d)?f.push(d.component.proxy):(v=d.children)!=null&&v.length?f.push(...a(d.children)):(p=d.component)!=null&&p.subTree&&f.push(...a(d.component.subTree))}),f},l=()=>{o.value=a(r.subTree)};return Ke(()=>{l()}),X1(n,l,{attributes:!0,subtree:!0,childList:!0}),{groupRef:n,visible:s,ns:t}}});function F3(e,t,n,r,o,s){return ct((L(),ee("ul",{ref:"groupRef",class:U(e.ns.be("group","wrap"))},[le("li",{class:U(e.ns.be("group","title"))},He(e.label),3),le("li",null,[le("ul",{class:U(e.ns.b("group"))},[de(e.$slots,"default")],2)])],2)),[[en,e.visible]])}var jg=Ne(k3,[["render",F3],["__file","option-group.vue"]]);const FN=yt($3,{Option:Kc,OptionGroup:jg}),BN=wo(Kc);wo(jg);const B3=xe({trigger:ei.trigger,placement:Ul.placement,disabled:ei.disabled,visible:Gt.visible,transition:Gt.transition,popperOptions:Ul.popperOptions,tabindex:Ul.tabindex,content:Gt.content,popperStyle:Gt.popperStyle,popperClass:Gt.popperClass,enterable:{...Gt.enterable,default:!0},effect:{...Gt.effect,default:"light"},teleported:Gt.teleported,title:String,width:{type:[String,Number],default:150},offset:{type:Number,default:void 0},showAfter:{type:Number,default:0},hideAfter:{type:Number,default:200},autoClose:{type:Number,default:0},showArrow:{type:Boolean,default:!0},persistent:{type:Boolean,default:!0},"onUpdate:visible":{type:Function}}),D3={"update:visible":e=>Bt(e),"before-enter":()=>!0,"before-leave":()=>!0,"after-enter":()=>!0,"after-leave":()=>!0},V3="onUpdate:visible",j3=Z({name:"ElPopover"}),z3=Z({...j3,props:B3,emits:D3,setup(e,{expose:t,emit:n}){const r=e,o=C(()=>r[V3]),s=$e("popover"),i=V(),a=C(()=>{var b;return(b=g(i))==null?void 0:b.popperRef}),l=C(()=>[{width:pn(r.width)},r.popperStyle]),u=C(()=>[s.b(),r.popperClass,{[s.m("plain")]:!!r.content}]),c=C(()=>r.transition===`${s.namespace.value}-fade-in-linear`),f=()=>{var b;(b=i.value)==null||b.hide()},d=()=>{n("before-enter")},v=()=>{n("before-leave")},p=()=>{n("after-enter")},h=()=>{n("update:visible",!1),n("after-leave")};return t({popperRef:a,hide:f}),(b,m)=>(L(),fe(g(Rg),En({ref_key:"tooltipRef",ref:i},b.$attrs,{trigger:b.trigger,placement:b.placement,disabled:b.disabled,visible:b.visible,transition:b.transition,"popper-options":b.popperOptions,tabindex:b.tabindex,content:b.content,offset:b.offset,"show-after":b.showAfter,"hide-after":b.hideAfter,"auto-close":b.autoClose,"show-arrow":b.showArrow,"aria-label":b.title,effect:b.effect,enterable:b.enterable,"popper-class":g(u),"popper-style":g(l),teleported:b.teleported,persistent:b.persistent,"gpu-acceleration":g(c),"onUpdate:visible":g(o),onBeforeShow:d,onBeforeHide:v,onShow:p,onHide:h}),{content:ce(()=>[b.title?(L(),ee("div",{key:0,class:U(g(s).e("title")),role:"title"},He(b.title),3)):ae("v-if",!0),de(b.$slots,"default",{},()=>[Un(He(b.content),1)])]),default:ce(()=>[b.$slots.reference?de(b.$slots,"reference",{key:0}):ae("v-if",!0)]),_:3},16,["trigger","placement","disabled","visible","transition","popper-options","tabindex","content","offset","show-after","hide-after","auto-close","show-arrow","aria-label","effect","enterable","popper-class","popper-style","teleported","persistent","gpu-acceleration","onUpdate:visible"]))}});var H3=Ne(z3,[["__file","popover.vue"]]);const Dp=(e,t)=>{const n=t.arg||t.value,r=n==null?void 0:n.popperRef;r&&(r.triggerRef=e)};var U3={mounted(e,t){Dp(e,t)},updated(e,t){Dp(e,t)}};const K3="popover",q3=KT(U3,K3),DN=yt(H3,{directive:q3}),W3=xe({modelValue:{type:[Boolean,String,Number],default:!1},disabled:Boolean,loading:Boolean,size:{type:String,validator:jm},width:{type:[String,Number],default:""},inlinePrompt:Boolean,inactiveActionIcon:{type:jt},activeActionIcon:{type:jt},activeIcon:{type:jt},inactiveIcon:{type:jt},activeText:{type:String,default:""},inactiveText:{type:String,default:""},activeValue:{type:[Boolean,String,Number],default:!0},inactiveValue:{type:[Boolean,String,Number],default:!1},name:{type:String,default:""},validateEvent:{type:Boolean,default:!0},beforeChange:{type:we(Function)},id:String,tabindex:{type:[String,Number]},...yr(["ariaLabel"])}),G3={[rt]:e=>Bt(e)||Ae(e)||je(e),[fo]:e=>Bt(e)||Ae(e)||je(e),[io]:e=>Bt(e)||Ae(e)||je(e)},zg="ElSwitch",Y3=Z({name:zg}),J3=Z({...Y3,props:W3,emits:G3,setup(e,{expose:t,emit:n}){const r=e,{formItem:o}=Vr(),s=In(),i=$e("switch"),{inputId:a}=hi(r,{formItemContext:o}),l=ns(C(()=>r.loading)),u=V(r.modelValue!==!1),c=V(),f=V(),d=C(()=>[i.b(),i.m(s.value),i.is("disabled",l.value),i.is("checked",m.value)]),v=C(()=>[i.e("label"),i.em("label","left"),i.is("active",!m.value)]),p=C(()=>[i.e("label"),i.em("label","right"),i.is("active",m.value)]),h=C(()=>({width:pn(r.width)}));he(()=>r.modelValue,()=>{u.value=!0});const b=C(()=>u.value?r.modelValue:!1),m=C(()=>b.value===r.activeValue);[r.activeValue,r.inactiveValue].includes(b.value)||(n(rt,r.inactiveValue),n(fo,r.inactiveValue),n(io,r.inactiveValue)),he(m,y=>{var A;c.value.checked=y,r.validateEvent&&((A=o==null?void 0:o.validate)==null||A.call(o,"change").catch(R=>void 0))});const S=()=>{const y=m.value?r.inactiveValue:r.activeValue;n(rt,y),n(fo,y),n(io,y),Ie(()=>{c.value.checked=m.value})},_=()=>{if(l.value)return;const{beforeChange:y}=r;if(!y){S();return}const A=y();[ia(A),Bt(A)].includes(!0)||Br(zg,"beforeChange must return type `Promise` or `boolean`"),ia(A)?A.then(N=>{N&&S()}).catch(N=>{}):A&&S()},w=()=>{var y,A;(A=(y=c.value)==null?void 0:y.focus)==null||A.call(y)};return Ke(()=>{c.value.checked=m.value}),t({focus:w,checked:m}),(y,A)=>(L(),ee("div",{class:U(g(d)),onClick:tt(_,["prevent"])},[le("input",{id:g(a),ref_key:"input",ref:c,class:U(g(i).e("input")),type:"checkbox",role:"switch","aria-checked":g(m),"aria-disabled":g(l),"aria-label":y.ariaLabel,name:y.name,"true-value":y.activeValue,"false-value":y.inactiveValue,disabled:g(l),tabindex:y.tabindex,onChange:S,onKeydown:Vt(_,["enter"])},null,42,["id","aria-checked","aria-disabled","aria-label","name","true-value","false-value","disabled","tabindex","onKeydown"]),!y.inlinePrompt&&(y.inactiveIcon||y.inactiveText)?(L(),ee("span",{key:0,class:U(g(v))},[y.inactiveIcon?(L(),fe(g(Je),{key:0},{default:ce(()=>[(L(),fe(Xe(y.inactiveIcon)))]),_:1})):ae("v-if",!0),!y.inactiveIcon&&y.inactiveText?(L(),ee("span",{key:1,"aria-hidden":g(m)},He(y.inactiveText),9,["aria-hidden"])):ae("v-if",!0)],2)):ae("v-if",!0),le("span",{ref_key:"core",ref:f,class:U(g(i).e("core")),style:ot(g(h))},[y.inlinePrompt?(L(),ee("div",{key:0,class:U(g(i).e("inner"))},[y.activeIcon||y.inactiveIcon?(L(),fe(g(Je),{key:0,class:U(g(i).is("icon"))},{default:ce(()=>[(L(),fe(Xe(g(m)?y.activeIcon:y.inactiveIcon)))]),_:1},8,["class"])):y.activeText||y.inactiveText?(L(),ee("span",{key:1,class:U(g(i).is("text")),"aria-hidden":!g(m)},He(g(m)?y.activeText:y.inactiveText),11,["aria-hidden"])):ae("v-if",!0)],2)):ae("v-if",!0),le("div",{class:U(g(i).e("action"))},[y.loading?(L(),fe(g(Je),{key:0,class:U(g(i).is("loading"))},{default:ce(()=>[re(g(Js))]),_:1},8,["class"])):g(m)?de(y.$slots,"active-action",{key:1},()=>[y.activeActionIcon?(L(),fe(g(Je),{key:0},{default:ce(()=>[(L(),fe(Xe(y.activeActionIcon)))]),_:1})):ae("v-if",!0)]):g(m)?ae("v-if",!0):de(y.$slots,"inactive-action",{key:2},()=>[y.inactiveActionIcon?(L(),fe(g(Je),{key:0},{default:ce(()=>[(L(),fe(Xe(y.inactiveActionIcon)))]),_:1})):ae("v-if",!0)])],2)],6),!y.inlinePrompt&&(y.activeIcon||y.activeText)?(L(),ee("span",{key:1,class:U(g(p))},[y.activeIcon?(L(),fe(g(Je),{key:0},{default:ce(()=>[(L(),fe(Xe(y.activeIcon)))]),_:1})):ae("v-if",!0),!y.activeIcon&&y.activeText?(L(),ee("span",{key:1,"aria-hidden":!g(m)},He(y.activeText),9,["aria-hidden"])):ae("v-if",!0)],2)):ae("v-if",!0)],10,["onClick"]))}});var X3=Ne(J3,[["__file","switch.vue"]]);const VN=yt(X3),fl=Symbol("tabsRootContextKey"),Z3=xe({tabs:{type:we(Array),default:()=>ol([])}}),Hg="ElTabBar",Q3=Z({name:Hg}),eI=Z({...Q3,props:Z3,setup(e,{expose:t}){const n=e,r=Ge(),o=Ee(fl);o||Br(Hg,"");const s=$e("tabs"),i=V(),a=V(),l=()=>{let v=0,p=0;const h=["top","bottom"].includes(o.props.tabPosition)?"width":"height",b=h==="width"?"x":"y",m=b==="x"?"left":"top";return n.tabs.every(S=>{var _,w;const y=(w=(_=r.parent)==null?void 0:_.refs)==null?void 0:w[`tab-${S.uid}`];if(!y)return!1;if(!S.active)return!0;v=y[`offset${Pr(m)}`],p=y[`client${Pr(h)}`];const A=window.getComputedStyle(y);return h==="width"&&(p-=Number.parseFloat(A.paddingLeft)+Number.parseFloat(A.paddingRight),v+=Number.parseFloat(A.paddingLeft)),!1}),{[h]:`${p}px`,transform:`translate${Pr(b)}(${v}px)`}},u=()=>a.value=l(),c=[],f=()=>{var v;c.forEach(h=>h.stop()),c.length=0;const p=(v=r.parent)==null?void 0:v.refs;if(p){for(const h in p)if(h.startsWith("tab-")){const b=p[h];b&&c.push(Dt(b,u))}}};he(()=>n.tabs,async()=>{await Ie(),u(),f()},{immediate:!0});const d=Dt(i,()=>u());return St(()=>{c.forEach(v=>v.stop()),c.length=0,d.stop()}),t({ref:i,update:u}),(v,p)=>(L(),ee("div",{ref_key:"barRef",ref:i,class:U([g(s).e("active-bar"),g(s).is(g(o).props.tabPosition)]),style:ot(a.value)},null,6))}});var tI=Ne(eI,[["__file","tab-bar.vue"]]);const nI=xe({panes:{type:we(Array),default:()=>ol([])},currentName:{type:[String,Number],default:""},editable:Boolean,type:{type:String,values:["card","border-card",""],default:""},stretch:Boolean}),rI={tabClick:(e,t,n)=>n instanceof Event,tabRemove:(e,t)=>t instanceof Event},Vp="ElTabNav",oI=Z({name:Vp,props:nI,emits:rI,setup(e,{expose:t,emit:n}){const r=Ee(fl);r||Br(Vp,"");const o=$e("tabs"),s=U1(),i=rS(),a=V(),l=V(),u=V(),c=V(),f=V(!1),d=V(0),v=V(!1),p=V(!0),h=C(()=>["top","bottom"].includes(r.props.tabPosition)?"width":"height"),b=C(()=>({transform:`translate${h.value==="width"?"X":"Y"}(-${d.value}px)`})),m=()=>{if(!a.value)return;const N=a.value[`offset${Pr(h.value)}`],I=d.value;if(!I)return;const x=I>N?I-N:0;d.value=x},S=()=>{if(!a.value||!l.value)return;const N=l.value[`offset${Pr(h.value)}`],I=a.value[`offset${Pr(h.value)}`],x=d.value;if(N-x<=I)return;const k=N-x>I*2?x+I:N-I;d.value=k},_=async()=>{const N=l.value;if(!f.value||!u.value||!a.value||!N)return;await Ie();const I=u.value.querySelector(".is-active");if(!I)return;const x=a.value,k=["top","bottom"].includes(r.props.tabPosition),$=I.getBoundingClientRect(),F=x.getBoundingClientRect(),Y=k?N.offsetWidth-F.width:N.offsetHeight-F.height,P=d.value;let O=P;k?($.leftF.right&&(O=P+$.right-F.right)):($.topF.bottom&&(O=P+($.bottom-F.bottom))),O=Math.max(O,0),d.value=Math.min(O,Y)},w=()=>{var N;if(!l.value||!a.value)return;e.stretch&&((N=c.value)==null||N.update());const I=l.value[`offset${Pr(h.value)}`],x=a.value[`offset${Pr(h.value)}`],k=d.value;x0&&(d.value=0))},y=N=>{const I=N.code,{up:x,down:k,left:$,right:F}=_n;if(![x,k,$,F].includes(I))return;const Y=Array.from(N.currentTarget.querySelectorAll("[role=tab]:not(.is-disabled)")),P=Y.indexOf(N.target);let O;I===$||I===x?P===0?O=Y.length-1:O=P-1:P{p.value&&(v.value=!0)},R=()=>v.value=!1;return he(s,N=>{N==="hidden"?p.value=!1:N==="visible"&&setTimeout(()=>p.value=!0,50)}),he(i,N=>{N?setTimeout(()=>p.value=!0,50):p.value=!1}),Dt(u,w),Ke(()=>setTimeout(()=>_(),0)),mo(()=>w()),t({scrollToActiveTab:_,removeFocus:R}),()=>{const N=f.value?[re("span",{class:[o.e("nav-prev"),o.is("disabled",!f.value.prev)],onClick:m},[re(Je,null,{default:()=>[re(hT,null,null)]})]),re("span",{class:[o.e("nav-next"),o.is("disabled",!f.value.next)],onClick:S},[re(Je,null,{default:()=>[re(mT,null,null)]})])]:null,I=e.panes.map((x,k)=>{var $,F,Y,P;const O=x.uid,j=x.props.disabled,Q=(F=($=x.props.name)!=null?$:x.index)!=null?F:`${k}`,me=!j&&(x.isClosable||e.editable);x.index=`${k}`;const Pe=me?re(Je,{class:"is-icon-close",onClick:_e=>n("tabRemove",x,_e)},{default:()=>[re(Ys,null,null)]}):null,Re=((P=(Y=x.slots).label)==null?void 0:P.call(Y))||x.props.label,Ce=!j&&x.active?0:-1;return re("div",{ref:`tab-${O}`,class:[o.e("item"),o.is(r.props.tabPosition),o.is("active",x.active),o.is("disabled",j),o.is("closable",me),o.is("focus",v.value)],id:`tab-${Q}`,key:`tab-${O}`,"aria-controls":`pane-${Q}`,role:"tab","aria-selected":x.active,tabindex:Ce,onFocus:()=>A(),onBlur:()=>R(),onClick:_e=>{R(),n("tabClick",x,Q,_e)},onKeydown:_e=>{me&&(_e.code===_n.delete||_e.code===_n.backspace)&&n("tabRemove",x,_e)}},[Re,Pe])});return re("div",{ref:u,class:[o.e("nav-wrap"),o.is("scrollable",!!f.value),o.is(r.props.tabPosition)]},[N,re("div",{class:o.e("nav-scroll"),ref:a},[re("div",{class:[o.e("nav"),o.is(r.props.tabPosition),o.is("stretch",e.stretch&&["top","bottom"].includes(r.props.tabPosition))],ref:l,style:b.value,role:"tablist",onKeydown:y},[e.type?null:re(tI,{ref:c,tabs:[...e.panes]},null),I])])])}}}),sI=xe({type:{type:String,values:["card","border-card",""],default:""},closable:Boolean,addable:Boolean,modelValue:{type:[String,Number]},editable:Boolean,tabPosition:{type:String,values:["top","right","bottom","left"],default:"top"},beforeLeave:{type:we(Function),default:()=>!0},stretch:Boolean}),Kl=e=>Ae(e)||je(e),iI={[rt]:e=>Kl(e),tabClick:(e,t)=>t instanceof Event,tabChange:e=>Kl(e),edit:(e,t)=>["remove","add"].includes(t),tabRemove:e=>Kl(e),tabAdd:()=>!0},aI=Z({name:"ElTabs",props:sI,emits:iI,setup(e,{emit:t,slots:n,expose:r}){var o;const s=$e("tabs"),i=C(()=>["left","right"].includes(e.tabPosition)),{children:a,addChild:l,removeChild:u}=yA(Ge(),"ElTabPane"),c=V(),f=V((o=e.modelValue)!=null?o:"0"),d=async(m,S=!1)=>{var _,w,y;if(!(f.value===m||Lt(m)))try{await((_=e.beforeLeave)==null?void 0:_.call(e,m,f.value))!==!1&&(f.value=m,S&&(t(rt,m),t("tabChange",m)),(y=(w=c.value)==null?void 0:w.removeFocus)==null||y.call(w))}catch{}},v=(m,S,_)=>{m.props.disabled||(d(S,!0),t("tabClick",m,_))},p=(m,S)=>{m.props.disabled||Lt(m.props.name)||(S.stopPropagation(),t("edit",m.props.name,"remove"),t("tabRemove",m.props.name))},h=()=>{t("edit",void 0,"add"),t("tabAdd")};he(()=>e.modelValue,m=>d(m)),he(f,async()=>{var m;await Ie(),(m=c.value)==null||m.scrollToActiveTab()}),ft(fl,{props:e,currentName:f,registerPane:m=>{a.value.push(m)},sortPane:l,unregisterPane:u}),r({currentName:f});const b=({render:m})=>m();return()=>{const m=n["add-icon"],S=e.editable||e.addable?re("div",{class:[s.e("new-tab"),i.value&&s.e("new-tab-vertical")],tabindex:"0",onClick:h,onKeydown:y=>{y.code===_n.enter&&h()}},[m?de(n,"add-icon"):re(Je,{class:s.is("icon-plus")},{default:()=>[re($m,null,null)]})]):null,_=re("div",{class:[s.e("header"),i.value&&s.e("header-vertical"),s.is(e.tabPosition)]},[re(b,{render:()=>{const y=a.value.some(A=>A.slots.label);return re(oI,{ref:c,currentName:f.value,editable:e.editable,type:e.type,panes:a.value,stretch:e.stretch,onTabClick:v,onTabRemove:p},{$stable:!y})}},null),S]),w=re("div",{class:s.e("content")},[de(n,"default")]);return re("div",{class:[s.b(),s.m(e.tabPosition),{[s.m("card")]:e.type==="card",[s.m("border-card")]:e.type==="border-card"}]},[w,_])}}}),lI=xe({label:{type:String,default:""},name:{type:[String,Number]},closable:Boolean,disabled:Boolean,lazy:Boolean}),Ug="ElTabPane",uI=Z({name:Ug}),cI=Z({...uI,props:lI,setup(e){const t=e,n=Ge(),r=go(),o=Ee(fl);o||Br(Ug,"usage: ");const s=$e("tab-pane"),i=V(),a=C(()=>t.closable||o.props.closable),l=fd(()=>{var v;return o.currentName.value===((v=t.name)!=null?v:i.value)}),u=V(l.value),c=C(()=>{var v;return(v=t.name)!=null?v:i.value}),f=fd(()=>!t.lazy||u.value||l.value);he(l,v=>{v&&(u.value=!0)});const d=wt({uid:n.uid,slots:r,props:t,paneName:c,active:l,index:i,isClosable:a});return o.registerPane(d),Ke(()=>{o.sortPane(d)}),kr(()=>{o.unregisterPane(d.uid)}),(v,p)=>g(f)?ct((L(),ee("div",{key:0,id:`pane-${g(c)}`,class:U(g(s).b()),role:"tabpanel","aria-hidden":!g(l),"aria-labelledby":`tab-${g(c)}`},[de(v.$slots,"default")],10,["id","aria-hidden","aria-labelledby"])),[[en,g(l)]]):ae("v-if",!0)}});var Kg=Ne(cI,[["__file","tab-pane.vue"]]);const jN=yt(aI,{TabPane:Kg}),zN=wo(Kg),fI=xe({type:{type:String,values:["primary","success","info","warning","danger",""],default:""},size:{type:String,values:es,default:""},truncated:Boolean,lineClamp:{type:[String,Number]},tag:{type:String,default:"span"}}),dI=Z({name:"ElText"}),pI=Z({...dI,props:fI,setup(e){const t=e,n=In(),r=$e("text"),o=C(()=>[r.b(),r.m(t.type),r.m(n.value),r.is("truncated",t.truncated),r.is("line-clamp",!Lt(t.lineClamp))]);return(s,i)=>(L(),fe(Xe(s.tag),{class:U(g(o)),style:ot({"-webkit-line-clamp":s.lineClamp})},{default:ce(()=>[de(s.$slots,"default")]),_:3},8,["class","style"]))}});var hI=Ne(pI,[["__file","text.vue"]]);const HN=yt(hI);function vI(e){let t;const n=V(!1),r=wt({...e,originalPosition:"",originalOverflow:"",visible:!1});function o(d){r.text=d}function s(){const d=r.parent,v=f.ns;if(!d.vLoadingAddClassList){let p=d.getAttribute("loading-number");p=Number.parseInt(p)-1,p?d.setAttribute("loading-number",p.toString()):(Gs(d,v.bm("parent","relative")),d.removeAttribute("loading-number")),Gs(d,v.bm("parent","hidden"))}i(),c.unmount()}function i(){var d,v;(v=(d=f.$el)==null?void 0:d.parentNode)==null||v.removeChild(f.$el)}function a(){var d;e.beforeClose&&!e.beforeClose()||(n.value=!0,clearTimeout(t),t=setTimeout(l,400),r.visible=!1,(d=e.closed)==null||d.call(e))}function l(){if(!n.value)return;const d=r.parent;n.value=!1,d.vLoadingAddClassList=void 0,s()}const c=gw(Z({name:"ElLoading",setup(d,{expose:v}){const{ns:p,zIndex:h}=Bc("loading");return v({ns:p,zIndex:h}),()=>{const b=r.spinner||r.svg,m=or("svg",{class:"circular",viewBox:r.svgViewBox?r.svgViewBox:"0 0 50 50",...b?{innerHTML:b}:{}},[or("circle",{class:"path",cx:"25",cy:"25",r:"20",fill:"none"})]),S=r.text?or("p",{class:p.b("text")},[r.text]):void 0;return or(Fr,{name:p.b("fade"),onAfterLeave:l},{default:ce(()=>[ct(re("div",{style:{backgroundColor:r.background||""},class:[p.b("mask"),r.customClass,r.fullscreen?"is-fullscreen":""]},[or("div",{class:p.b("spinner")},[m,S])]),[[en,r.visible]])])})}}})),f=c.mount(document.createElement("div"));return{...vr(r),setText:o,removeElLoadingChild:i,close:a,handleAfterLeave:l,vm:f,get $el(){return f.$el}}}let Bi;const Fu=function(e={}){if(!st)return;const t=mI(e);if(t.fullscreen&&Bi)return Bi;const n=vI({...t,closed:()=>{var o;(o=t.closed)==null||o.call(t),t.fullscreen&&(Bi=void 0)}});gI(t,t.parent,n),jp(t,t.parent,n),t.parent.vLoadingAddClassList=()=>jp(t,t.parent,n);let r=t.parent.getAttribute("loading-number");return r?r=`${Number.parseInt(r)+1}`:r="1",t.parent.setAttribute("loading-number",r),t.parent.appendChild(n.$el),Ie(()=>n.visible.value=t.visible),t.fullscreen&&(Bi=n),n},mI=e=>{var t,n,r,o;let s;return Ae(e.target)?s=(t=document.querySelector(e.target))!=null?t:document.body:s=e.target||document.body,{parent:s===document.body||e.body?document.body:s,background:e.background||"",svg:e.svg||"",svgViewBox:e.svgViewBox||"",spinner:e.spinner||!1,text:e.text||"",fullscreen:s===document.body&&((n=e.fullscreen)!=null?n:!0),lock:(r=e.lock)!=null?r:!1,customClass:e.customClass||"",visible:(o=e.visible)!=null?o:!0,beforeClose:e.beforeClose,closed:e.closed,target:s}},gI=async(e,t,n)=>{const{nextZIndex:r}=n.vm.zIndex||n.vm._.exposed.zIndex,o={};if(e.fullscreen)n.originalPosition.value=Po(document.body,"position"),n.originalOverflow.value=Po(document.body,"overflow"),o.zIndex=r();else if(e.parent===document.body){n.originalPosition.value=Po(document.body,"position"),await Ie();for(const s of["top","left"]){const i=s==="top"?"scrollTop":"scrollLeft";o[s]=`${e.target.getBoundingClientRect()[s]+document.body[i]+document.documentElement[i]-Number.parseInt(Po(document.body,`margin-${s}`),10)}px`}for(const s of["height","width"])o[s]=`${e.target.getBoundingClientRect()[s]}px`}else n.originalPosition.value=Po(t,"position");for(const[s,i]of Object.entries(o))n.$el.style[s]=i},jp=(e,t,n)=>{const r=n.vm.ns||n.vm._.exposed.ns;["absolute","fixed","sticky"].includes(n.originalPosition.value)?Gs(t,r.bm("parent","relative")):Tu(t,r.bm("parent","relative")),e.fullscreen&&e.lock?Tu(t,r.bm("parent","hidden")):Gs(t,r.bm("parent","hidden"))},na=Symbol("ElLoading"),zp=(e,t)=>{var n,r,o,s;const i=t.instance,a=d=>Oe(t.value)?t.value[d]:void 0,l=d=>{const v=Ae(d)&&(i==null?void 0:i[d])||d;return v&&V(v)},u=d=>l(a(d)||e.getAttribute(`element-loading-${hr(d)}`)),c=(n=a("fullscreen"))!=null?n:t.modifiers.fullscreen,f={text:u("text"),svg:u("svg"),svgViewBox:u("svgViewBox"),spinner:u("spinner"),background:u("background"),customClass:u("customClass"),fullscreen:c,target:(r=a("target"))!=null?r:c?void 0:e,body:(o=a("body"))!=null?o:t.modifiers.body,lock:(s=a("lock"))!=null?s:t.modifiers.lock};e[na]={options:f,instance:Fu(f)}},bI=(e,t)=>{for(const n of Object.keys(t))Ue(t[n])&&(t[n].value=e[n])},Hp={mounted(e,t){t.value&&zp(e,t)},updated(e,t){const n=e[na];t.oldValue!==t.value&&(t.value&&!t.oldValue?zp(e,t):t.value&&t.oldValue?Oe(t.value)&&bI(t.value,n.options):n==null||n.instance.close())},unmounted(e){var t;(t=e[na])==null||t.instance.close(),e[na]=null}},UN={install(e){e.directive("loading",Hp),e.config.globalProperties.$loading=Fu},directive:Hp,service:Fu},qg=["success","info","warning","error"],At=ol({customClass:"",center:!1,dangerouslyUseHTMLString:!1,duration:3e3,icon:void 0,id:"",message:"",onClose:void 0,showClose:!1,type:"info",plain:!1,offset:16,zIndex:0,grouping:!1,repeatNum:1,appendTo:st?document.body:void 0}),yI=xe({customClass:{type:String,default:At.customClass},center:{type:Boolean,default:At.center},dangerouslyUseHTMLString:{type:Boolean,default:At.dangerouslyUseHTMLString},duration:{type:Number,default:At.duration},icon:{type:jt,default:At.icon},id:{type:String,default:At.id},message:{type:we([String,Object,Function]),default:At.message},onClose:{type:we(Function),default:At.onClose},showClose:{type:Boolean,default:At.showClose},type:{type:String,values:qg,default:At.type},plain:{type:Boolean,default:At.plain},offset:{type:Number,default:At.offset},zIndex:{type:Number,default:At.zIndex},grouping:{type:Boolean,default:At.grouping},repeatNum:{type:Number,default:At.repeatNum}}),wI={destroy:()=>!0},wn=Qu([]),SI=e=>{const t=wn.findIndex(o=>o.id===e),n=wn[t];let r;return t>0&&(r=wn[t-1]),{current:n,prev:r}},EI=e=>{const{prev:t}=SI(e);return t?t.vm.exposed.bottom.value:0},_I=(e,t)=>wn.findIndex(r=>r.id===e)>0?16:t,CI=Z({name:"ElMessage"}),TI=Z({...CI,props:yI,emits:wI,setup(e,{expose:t}){const n=e,{Close:r}=Dm,{ns:o,zIndex:s}=Bc("message"),{currentZIndex:i,nextZIndex:a}=s,l=V(),u=V(!1),c=V(0);let f;const d=C(()=>n.type?n.type==="error"?"danger":n.type:"info"),v=C(()=>{const R=n.type;return{[o.bm("icon",R)]:R&&Ra[R]}}),p=C(()=>n.icon||Ra[n.type]||""),h=C(()=>EI(n.id)),b=C(()=>_I(n.id,n.offset)+h.value),m=C(()=>c.value+b.value),S=C(()=>({top:`${b.value}px`,zIndex:i.value}));function _(){n.duration!==0&&({stop:f}=bu(()=>{y()},n.duration))}function w(){f==null||f()}function y(){u.value=!1}function A({code:R}){R===_n.esc&&y()}return Ke(()=>{_(),a(),u.value=!0}),he(()=>n.repeatNum,()=>{w(),_()}),tn(document,"keydown",A),Dt(l,()=>{c.value=l.value.getBoundingClientRect().height}),t({visible:u,bottom:m,close:y}),(R,N)=>(L(),fe(Fr,{name:g(o).b("fade"),onBeforeLeave:R.onClose,onAfterLeave:I=>R.$emit("destroy"),persisted:""},{default:ce(()=>[ct(le("div",{id:R.id,ref_key:"messageRef",ref:l,class:U([g(o).b(),{[g(o).m(R.type)]:R.type},g(o).is("center",R.center),g(o).is("closable",R.showClose),g(o).is("plain",R.plain),R.customClass]),style:ot(g(S)),role:"alert",onMouseenter:w,onMouseleave:_},[R.repeatNum>1?(L(),fe(g(XR),{key:0,value:R.repeatNum,type:g(d),class:U(g(o).e("badge"))},null,8,["value","type","class"])):ae("v-if",!0),g(p)?(L(),fe(g(Je),{key:1,class:U([g(o).e("icon"),g(v)])},{default:ce(()=>[(L(),fe(Xe(g(p))))]),_:1},8,["class"])):ae("v-if",!0),de(R.$slots,"default",{},()=>[R.dangerouslyUseHTMLString?(L(),ee(nt,{key:1},[ae(" Caution here, message could've been compromised, never use user's input as message "),le("p",{class:U(g(o).e("content")),innerHTML:R.message},null,10,["innerHTML"])],2112)):(L(),ee("p",{key:0,class:U(g(o).e("content"))},He(R.message),3))]),R.showClose?(L(),fe(g(Je),{key:2,class:U(g(o).e("closeBtn")),onClick:tt(y,["stop"])},{default:ce(()=>[re(g(r))]),_:1},8,["class","onClick"])):ae("v-if",!0)],46,["id"]),[[en,u.value]])]),_:3},8,["name","onBeforeLeave","onAfterLeave"]))}});var OI=Ne(TI,[["__file","message.vue"]]);let AI=1;const Wg=e=>{const t=!e||Ae(e)||cn(e)||ve(e)?{message:e}:e,n={...At,...t};if(!n.appendTo)n.appendTo=document.body;else if(Ae(n.appendTo)){let r=document.querySelector(n.appendTo);ar(r)||(r=document.body),n.appendTo=r}return Bt(Fn.grouping)&&!n.grouping&&(n.grouping=Fn.grouping),je(Fn.duration)&&n.duration===3e3&&(n.duration=Fn.duration),je(Fn.offset)&&n.offset===16&&(n.offset=Fn.offset),Bt(Fn.showClose)&&!n.showClose&&(n.showClose=Fn.showClose),n},RI=e=>{const t=wn.indexOf(e);if(t===-1)return;wn.splice(t,1);const{handler:n}=e;n.close()},xI=({appendTo:e,...t},n)=>{const r=`message_${AI++}`,o=t.onClose,s=document.createElement("div"),i={...t,id:r,onClose:()=>{o==null||o(),RI(c)},onDestroy:()=>{wa(null,s)}},a=re(OI,i,ve(i.message)||cn(i.message)?{default:ve(i.message)?i.message:()=>i.message}:null);a.appContext=n||Go._context,wa(a,s),e.appendChild(s.firstElementChild);const l=a.component,c={id:r,vnode:a,vm:l,handler:{close:()=>{l.exposed.visible.value=!1}},props:a.component.props};return c},Go=(e={},t)=>{if(!st)return{close:()=>{}};const n=Wg(e);if(n.grouping&&wn.length){const o=wn.find(({vnode:s})=>{var i;return((i=s.props)==null?void 0:i.message)===n.message});if(o)return o.props.repeatNum+=1,o.props.type=n.type,o.handler}if(je(Fn.max)&&wn.length>=Fn.max)return{close:()=>{}};const r=xI(n,t);return wn.push(r),r.handler};qg.forEach(e=>{Go[e]=(t={},n)=>{const r=Wg(t);return Go({...r,type:e},n)}});function II(e){for(const t of wn)(!e||e===t.props.type)&&t.handler.close()}Go.closeAll=II;Go._context=null;const KN=UT(Go,"$message"),PI=Z({name:"ElMessageBox",directives:{TrapFocus:Ox},components:{ElButton:Ex,ElFocusTrap:Hc,ElInput:bg,ElOverlay:Fg,ElIcon:Je,...Dm},inheritAttrs:!1,props:{buttonSize:{type:String,validator:jm},modal:{type:Boolean,default:!0},lockScroll:{type:Boolean,default:!0},showClose:{type:Boolean,default:!0},closeOnClickModal:{type:Boolean,default:!0},closeOnPressEscape:{type:Boolean,default:!0},closeOnHashChange:{type:Boolean,default:!0},center:Boolean,draggable:Boolean,overflow:Boolean,roundButton:{default:!1,type:Boolean},container:{type:String,default:"body"},boxType:{type:String,default:""}},emits:["vanish","action"],setup(e,{emit:t}){const{locale:n,zIndex:r,ns:o,size:s}=Bc("message-box",C(()=>e.buttonSize)),{t:i}=n,{nextZIndex:a}=r,l=V(!1),u=wt({autofocus:!0,beforeClose:null,callback:null,cancelButtonText:"",cancelButtonClass:"",confirmButtonText:"",confirmButtonClass:"",customClass:"",customStyle:{},dangerouslyUseHTMLString:!1,distinguishCancelAndClose:!1,icon:"",inputPattern:null,inputPlaceholder:"",inputType:"text",inputValue:null,inputValidator:null,inputErrorMessage:"",message:null,modalFade:!0,modalClass:"",showCancelButton:!1,showConfirmButton:!0,type:"",title:void 0,showInput:!1,action:"",confirmButtonLoading:!1,cancelButtonLoading:!1,confirmButtonLoadingIcon:Ds(Js),cancelButtonLoadingIcon:Ds(Js),confirmButtonDisabled:!1,editorErrorMessage:"",validateError:!1,zIndex:a()}),c=C(()=>{const O=u.type;return{[o.bm("icon",O)]:O&&Ra[O]}}),f=pr(),d=pr(),v=C(()=>u.icon||Ra[u.type]||""),p=C(()=>!!u.message),h=V(),b=V(),m=V(),S=V(),_=V(),w=C(()=>u.confirmButtonClass);he(()=>u.inputValue,async O=>{await Ie(),e.boxType==="prompt"&&O!==null&&$()},{immediate:!0}),he(()=>l.value,O=>{var j,Q;O&&(e.boxType!=="prompt"&&(u.autofocus?m.value=(Q=(j=_.value)==null?void 0:j.$el)!=null?Q:h.value:m.value=h.value),u.zIndex=a()),e.boxType==="prompt"&&(O?Ie().then(()=>{var me;S.value&&S.value.$el&&(u.autofocus?m.value=(me=F())!=null?me:h.value:m.value=h.value)}):(u.editorErrorMessage="",u.validateError=!1))});const y=C(()=>e.draggable),A=C(()=>e.overflow);zm(h,b,y,A),Ke(async()=>{await Ie(),e.closeOnHashChange&&window.addEventListener("hashchange",R)}),St(()=>{e.closeOnHashChange&&window.removeEventListener("hashchange",R)});function R(){l.value&&(l.value=!1,Ie(()=>{u.action&&t("action",u.action)}))}const N=()=>{e.closeOnClickModal&&k(u.distinguishCancelAndClose?"close":"cancel")},I=kc(N),x=O=>{if(u.inputType!=="textarea")return O.preventDefault(),k("confirm")},k=O=>{var j;e.boxType==="prompt"&&O==="confirm"&&!$()||(u.action=O,u.beforeClose?(j=u.beforeClose)==null||j.call(u,O,u,R):R())},$=()=>{if(e.boxType==="prompt"){const O=u.inputPattern;if(O&&!O.test(u.inputValue||""))return u.editorErrorMessage=u.inputErrorMessage||i("el.messagebox.error"),u.validateError=!0,!1;const j=u.inputValidator;if(typeof j=="function"){const Q=j(u.inputValue);if(Q===!1)return u.editorErrorMessage=u.inputErrorMessage||i("el.messagebox.error"),u.validateError=!0,!1;if(typeof Q=="string")return u.editorErrorMessage=Q,u.validateError=!0,!1}}return u.editorErrorMessage="",u.validateError=!1,!0},F=()=>{const O=S.value.$refs;return O.input||O.textarea},Y=()=>{k("close")},P=()=>{e.closeOnPressEscape&&Y()};return e.lockScroll&&Km(l),{...vr(u),ns:o,overlayEvent:I,visible:l,hasMessage:p,typeClass:c,contentId:f,inputId:d,btnSize:s,iconComponent:v,confirmButtonClasses:w,rootRef:h,focusStartRef:m,headerRef:b,inputRef:S,confirmRef:_,doClose:R,handleClose:Y,onCloseRequested:P,handleWrapperClick:N,handleInputEnter:x,handleAction:k,t:i}}});function NI(e,t,n,r,o,s){const i=Yt("el-icon"),a=Yt("close"),l=Yt("el-input"),u=Yt("el-button"),c=Yt("el-focus-trap"),f=Yt("el-overlay");return L(),fe(Fr,{name:"fade-in-linear",onAfterLeave:d=>e.$emit("vanish"),persisted:""},{default:ce(()=>[ct(re(f,{"z-index":e.zIndex,"overlay-class":[e.ns.is("message-box"),e.modalClass],mask:e.modal},{default:ce(()=>[le("div",{role:"dialog","aria-label":e.title,"aria-modal":"true","aria-describedby":e.showInput?void 0:e.contentId,class:U(`${e.ns.namespace.value}-overlay-message-box`),onClick:e.overlayEvent.onClick,onMousedown:e.overlayEvent.onMousedown,onMouseup:e.overlayEvent.onMouseup},[re(c,{loop:"",trapped:e.visible,"focus-trap-el":e.rootRef,"focus-start-el":e.focusStartRef,onReleaseRequested:e.onCloseRequested},{default:ce(()=>[le("div",{ref:"rootRef",class:U([e.ns.b(),e.customClass,e.ns.is("draggable",e.draggable),{[e.ns.m("center")]:e.center}]),style:ot(e.customStyle),tabindex:"-1",onClick:tt(()=>{},["stop"])},[e.title!==null&&e.title!==void 0?(L(),ee("div",{key:0,ref:"headerRef",class:U([e.ns.e("header"),{"show-close":e.showClose}])},[le("div",{class:U(e.ns.e("title"))},[e.iconComponent&&e.center?(L(),fe(i,{key:0,class:U([e.ns.e("status"),e.typeClass])},{default:ce(()=>[(L(),fe(Xe(e.iconComponent)))]),_:1},8,["class"])):ae("v-if",!0),le("span",null,He(e.title),1)],2),e.showClose?(L(),ee("button",{key:0,type:"button",class:U(e.ns.e("headerbtn")),"aria-label":e.t("el.messagebox.close"),onClick:d=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel"),onKeydown:Vt(tt(d=>e.handleAction(e.distinguishCancelAndClose?"close":"cancel"),["prevent"]),["enter"])},[re(i,{class:U(e.ns.e("close"))},{default:ce(()=>[re(a)]),_:1},8,["class"])],42,["aria-label","onClick","onKeydown"])):ae("v-if",!0)],2)):ae("v-if",!0),le("div",{id:e.contentId,class:U(e.ns.e("content"))},[le("div",{class:U(e.ns.e("container"))},[e.iconComponent&&!e.center&&e.hasMessage?(L(),fe(i,{key:0,class:U([e.ns.e("status"),e.typeClass])},{default:ce(()=>[(L(),fe(Xe(e.iconComponent)))]),_:1},8,["class"])):ae("v-if",!0),e.hasMessage?(L(),ee("div",{key:1,class:U(e.ns.e("message"))},[de(e.$slots,"default",{},()=>[e.dangerouslyUseHTMLString?(L(),fe(Xe(e.showInput?"label":"p"),{key:1,for:e.showInput?e.inputId:void 0,innerHTML:e.message},null,8,["for","innerHTML"])):(L(),fe(Xe(e.showInput?"label":"p"),{key:0,for:e.showInput?e.inputId:void 0},{default:ce(()=>[Un(He(e.dangerouslyUseHTMLString?"":e.message),1)]),_:1},8,["for"]))])],2)):ae("v-if",!0)],2),ct(le("div",{class:U(e.ns.e("input"))},[re(l,{id:e.inputId,ref:"inputRef",modelValue:e.inputValue,"onUpdate:modelValue":d=>e.inputValue=d,type:e.inputType,placeholder:e.inputPlaceholder,"aria-invalid":e.validateError,class:U({invalid:e.validateError}),onKeydown:Vt(e.handleInputEnter,["enter"])},null,8,["id","modelValue","onUpdate:modelValue","type","placeholder","aria-invalid","class","onKeydown"]),le("div",{class:U(e.ns.e("errormsg")),style:ot({visibility:e.editorErrorMessage?"visible":"hidden"})},He(e.editorErrorMessage),7)],2),[[en,e.showInput]])],10,["id"]),le("div",{class:U(e.ns.e("btns"))},[e.showCancelButton?(L(),fe(u,{key:0,loading:e.cancelButtonLoading,"loading-icon":e.cancelButtonLoadingIcon,class:U([e.cancelButtonClass]),round:e.roundButton,size:e.btnSize,onClick:d=>e.handleAction("cancel"),onKeydown:Vt(tt(d=>e.handleAction("cancel"),["prevent"]),["enter"])},{default:ce(()=>[Un(He(e.cancelButtonText||e.t("el.messagebox.cancel")),1)]),_:1},8,["loading","loading-icon","class","round","size","onClick","onKeydown"])):ae("v-if",!0),ct(re(u,{ref:"confirmRef",type:"primary",loading:e.confirmButtonLoading,"loading-icon":e.confirmButtonLoadingIcon,class:U([e.confirmButtonClasses]),round:e.roundButton,disabled:e.confirmButtonDisabled,size:e.btnSize,onClick:d=>e.handleAction("confirm"),onKeydown:Vt(tt(d=>e.handleAction("confirm"),["prevent"]),["enter"])},{default:ce(()=>[Un(He(e.confirmButtonText||e.t("el.messagebox.confirm")),1)]),_:1},8,["loading","loading-icon","class","round","disabled","size","onClick","onKeydown"]),[[en,e.showConfirmButton]])],2)],14,["onClick"])]),_:3},8,["trapped","focus-trap-el","focus-start-el","onReleaseRequested"])],42,["aria-label","aria-describedby","onClick","onMousedown","onMouseup"])]),_:3},8,["z-index","overlay-class","mask"]),[[en,e.visible]])]),_:3},8,["onAfterLeave"])}var LI=Ne(PI,[["render",NI],["__file","index.vue"]]);const ti=new Map,MI=e=>{let t=document.body;return e.appendTo&&(Ae(e.appendTo)&&(t=document.querySelector(e.appendTo)),ar(e.appendTo)&&(t=e.appendTo),ar(t)||(t=document.body)),t},$I=(e,t,n=null)=>{const r=re(LI,e,ve(e.message)||cn(e.message)?{default:ve(e.message)?e.message:()=>e.message}:null);return r.appContext=n,wa(r,t),MI(e).appendChild(t.firstElementChild),r.component},kI=()=>document.createElement("div"),FI=(e,t)=>{const n=kI();e.onVanish=()=>{wa(null,n),ti.delete(o)},e.onAction=s=>{const i=ti.get(o);let a;e.showInput?a={value:o.inputValue,action:s}:a=s,e.callback?e.callback(a,r.proxy):s==="cancel"||s==="close"?e.distinguishCancelAndClose&&s!=="cancel"?i.reject("close"):i.reject("cancel"):i.resolve(a)};const r=$I(e,n,t),o=r.proxy;for(const s in e)Ve(e,s)&&!Ve(o.$props,s)&&(o[s]=e[s]);return o.visible=!0,o};function os(e,t=null){if(!st)return Promise.reject();let n;return Ae(e)||cn(e)?e={message:e}:n=e.callback,new Promise((r,o)=>{const s=FI(e,t??os._context);ti.set(s,{options:e,callback:n,resolve:r,reject:o})})}const BI=["alert","confirm","prompt"],DI={alert:{closeOnPressEscape:!1,closeOnClickModal:!1},confirm:{showCancelButton:!0},prompt:{showCancelButton:!0,showInput:!0}};BI.forEach(e=>{os[e]=VI(e)});function VI(e){return(t,n,r,o)=>{let s="";return Oe(n)?(r=n,s=""):Lt(n)?s="":s=n,os(Object.assign({title:s,message:t,type:"",...DI[e]},r,{boxType:e}),o)}}os.close=()=>{ti.forEach((e,t)=>{t.doClose()}),ti.clear()};os._context=null;const Ar=os;Ar.install=e=>{Ar._context=e._context,e.config.globalProperties.$msgbox=Ar,e.config.globalProperties.$messageBox=Ar,e.config.globalProperties.$alert=Ar.alert,e.config.globalProperties.$confirm=Ar.confirm,e.config.globalProperties.$prompt=Ar.prompt};const qN=Ar;function Gg(e,t){return function(){return e.apply(t,arguments)}}const{toString:jI}=Object.prototype,{getPrototypeOf:qc}=Object,{iterator:dl,toStringTag:Yg}=Symbol,pl=(e=>t=>{const n=jI.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),Nn=e=>(e=e.toLowerCase(),t=>pl(t)===e),hl=e=>t=>typeof t===e,{isArray:ss}=Array,Yo=hl("undefined");function mi(e){return e!==null&&!Yo(e)&&e.constructor!==null&&!Yo(e.constructor)&&Ut(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const Jg=Nn("ArrayBuffer");function zI(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&Jg(e.buffer),t}const HI=hl("string"),Ut=hl("function"),Xg=hl("number"),gi=e=>e!==null&&typeof e=="object",UI=e=>e===!0||e===!1,ra=e=>{if(pl(e)!=="object")return!1;const t=qc(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Yg in e)&&!(dl in e)},KI=e=>{if(!gi(e)||mi(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},qI=Nn("Date"),WI=Nn("File"),GI=e=>!!(e&&typeof e.uri<"u"),YI=e=>e&&typeof e.getParts<"u",JI=Nn("Blob"),XI=Nn("FileList"),ZI=e=>gi(e)&&Ut(e.pipe);function QI(){return typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}}const Up=QI(),Kp=typeof Up.FormData<"u"?Up.FormData:void 0,eP=e=>{let t;return e&&(Kp&&e instanceof Kp||Ut(e.append)&&((t=pl(e))==="formdata"||t==="object"&&Ut(e.toString)&&e.toString()==="[object FormData]"))},tP=Nn("URLSearchParams"),[nP,rP,oP,sP]=["ReadableStream","Request","Response","Headers"].map(Nn),iP=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function bi(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;let r,o;if(typeof e!="object"&&(e=[e]),ss(e))for(r=0,o=e.length;r0;)if(o=n[r],t===o.toLowerCase())return o;return null}const ro=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,Qg=e=>!Yo(e)&&e!==ro;function Bu(){const{caseless:e,skipUndefined:t}=Qg(this)&&this||{},n={},r=(o,s)=>{if(s==="__proto__"||s==="constructor"||s==="prototype")return;const i=e&&Zg(n,s)||s;ra(n[i])&&ra(o)?n[i]=Bu(n[i],o):ra(o)?n[i]=Bu({},o):ss(o)?n[i]=o.slice():(!t||!Yo(o))&&(n[i]=o)};for(let o=0,s=arguments.length;o(bi(t,(o,s)=>{n&&Ut(o)?Object.defineProperty(e,s,{value:Gg(o,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(e,s,{value:o,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:r}),e),lP=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),uP=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},cP=(e,t,n,r)=>{let o,s,i;const a={};if(t=t||{},e==null)return t;do{for(o=Object.getOwnPropertyNames(e),s=o.length;s-- >0;)i=o[s],(!r||r(i,e,t))&&!a[i]&&(t[i]=e[i],a[i]=!0);e=n!==!1&&qc(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},fP=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return r!==-1&&r===n},dP=e=>{if(!e)return null;if(ss(e))return e;let t=e.length;if(!Xg(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},pP=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&qc(Uint8Array)),hP=(e,t)=>{const r=(e&&e[dl]).call(e);let o;for(;(o=r.next())&&!o.done;){const s=o.value;t.call(e,s[0],s[1])}},vP=(e,t)=>{let n;const r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},mP=Nn("HTMLFormElement"),gP=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,r,o){return r.toUpperCase()+o}),qp=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),bP=Nn("RegExp"),eb=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};bi(n,(o,s)=>{let i;(i=t(o,s,e))!==!1&&(r[s]=i||o)}),Object.defineProperties(e,r)},yP=e=>{eb(e,(t,n)=>{if(Ut(e)&&["arguments","caller","callee"].indexOf(n)!==-1)return!1;const r=e[n];if(Ut(r)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},wP=(e,t)=>{const n={},r=o=>{o.forEach(s=>{n[s]=!0})};return ss(e)?r(e):r(String(e).split(t)),n},SP=()=>{},EP=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function _P(e){return!!(e&&Ut(e.append)&&e[Yg]==="FormData"&&e[dl])}const CP=e=>{const t=new Array(10),n=(r,o)=>{if(gi(r)){if(t.indexOf(r)>=0)return;if(mi(r))return r;if(!("toJSON"in r)){t[o]=r;const s=ss(r)?[]:{};return bi(r,(i,a)=>{const l=n(i,o+1);!Yo(l)&&(s[a]=l)}),t[o]=void 0,s}}return r};return n(e,0)},TP=Nn("AsyncFunction"),OP=e=>e&&(gi(e)||Ut(e))&&Ut(e.then)&&Ut(e.catch),tb=((e,t)=>e?setImmediate:t?((n,r)=>(ro.addEventListener("message",({source:o,data:s})=>{o===ro&&s===n&&r.length&&r.shift()()},!1),o=>{r.push(o),ro.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",Ut(ro.postMessage)),AP=typeof queueMicrotask<"u"?queueMicrotask.bind(ro):typeof process<"u"&&process.nextTick||tb,RP=e=>e!=null&&Ut(e[dl]),D={isArray:ss,isArrayBuffer:Jg,isBuffer:mi,isFormData:eP,isArrayBufferView:zI,isString:HI,isNumber:Xg,isBoolean:UI,isObject:gi,isPlainObject:ra,isEmptyObject:KI,isReadableStream:nP,isRequest:rP,isResponse:oP,isHeaders:sP,isUndefined:Yo,isDate:qI,isFile:WI,isReactNativeBlob:GI,isReactNative:YI,isBlob:JI,isRegExp:bP,isFunction:Ut,isStream:ZI,isURLSearchParams:tP,isTypedArray:pP,isFileList:XI,forEach:bi,merge:Bu,extend:aP,trim:iP,stripBOM:lP,inherits:uP,toFlatObject:cP,kindOf:pl,kindOfTest:Nn,endsWith:fP,toArray:dP,forEachEntry:hP,matchAll:vP,isHTMLForm:mP,hasOwnProperty:qp,hasOwnProp:qp,reduceDescriptors:eb,freezeMethods:yP,toObjectSet:wP,toCamelCase:gP,noop:SP,toFiniteNumber:EP,findKey:Zg,global:ro,isContextDefined:Qg,isSpecCompliantForm:_P,toJSONObject:CP,isAsyncFn:TP,isThenable:OP,setImmediate:tb,asap:AP,isIterable:RP};let Te=class nb extends Error{static from(t,n,r,o,s,i){const a=new nb(t.message,n||t.code,r,o,s);return a.cause=t,a.name=t.name,t.status!=null&&a.status==null&&(a.status=t.status),i&&Object.assign(a,i),a}constructor(t,n,r,o,s){super(t),Object.defineProperty(this,"message",{value:t,enumerable:!0,writable:!0,configurable:!0}),this.name="AxiosError",this.isAxiosError=!0,n&&(this.code=n),r&&(this.config=r),o&&(this.request=o),s&&(this.response=s,this.status=s.status)}toJSON(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:D.toJSONObject(this.config),code:this.code,status:this.status}}};Te.ERR_BAD_OPTION_VALUE="ERR_BAD_OPTION_VALUE";Te.ERR_BAD_OPTION="ERR_BAD_OPTION";Te.ECONNABORTED="ECONNABORTED";Te.ETIMEDOUT="ETIMEDOUT";Te.ERR_NETWORK="ERR_NETWORK";Te.ERR_FR_TOO_MANY_REDIRECTS="ERR_FR_TOO_MANY_REDIRECTS";Te.ERR_DEPRECATED="ERR_DEPRECATED";Te.ERR_BAD_RESPONSE="ERR_BAD_RESPONSE";Te.ERR_BAD_REQUEST="ERR_BAD_REQUEST";Te.ERR_CANCELED="ERR_CANCELED";Te.ERR_NOT_SUPPORT="ERR_NOT_SUPPORT";Te.ERR_INVALID_URL="ERR_INVALID_URL";const xP=null;function Du(e){return D.isPlainObject(e)||D.isArray(e)}function rb(e){return D.endsWith(e,"[]")?e.slice(0,-2):e}function ql(e,t,n){return e?e.concat(t).map(function(o,s){return o=rb(o),!n&&s?"["+o+"]":o}).join(n?".":""):t}function IP(e){return D.isArray(e)&&!e.some(Du)}const PP=D.toFlatObject(D,{},null,function(t){return/^is[A-Z]/.test(t)});function vl(e,t,n){if(!D.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,n=D.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(h,b){return!D.isUndefined(b[h])});const r=n.metaTokens,o=n.visitor||c,s=n.dots,i=n.indexes,l=(n.Blob||typeof Blob<"u"&&Blob)&&D.isSpecCompliantForm(t);if(!D.isFunction(o))throw new TypeError("visitor must be a function");function u(p){if(p===null)return"";if(D.isDate(p))return p.toISOString();if(D.isBoolean(p))return p.toString();if(!l&&D.isBlob(p))throw new Te("Blob is not supported. Use a Buffer instead.");return D.isArrayBuffer(p)||D.isTypedArray(p)?l&&typeof Blob=="function"?new Blob([p]):Buffer.from(p):p}function c(p,h,b){let m=p;if(D.isReactNative(t)&&D.isReactNativeBlob(p))return t.append(ql(b,h,s),u(p)),!1;if(p&&!b&&typeof p=="object"){if(D.endsWith(h,"{}"))h=r?h:h.slice(0,-2),p=JSON.stringify(p);else if(D.isArray(p)&&IP(p)||(D.isFileList(p)||D.endsWith(h,"[]"))&&(m=D.toArray(p)))return h=rb(h),m.forEach(function(_,w){!(D.isUndefined(_)||_===null)&&t.append(i===!0?ql([h],w,s):i===null?h:h+"[]",u(_))}),!1}return Du(p)?!0:(t.append(ql(b,h,s),u(p)),!1)}const f=[],d=Object.assign(PP,{defaultVisitor:c,convertValue:u,isVisitable:Du});function v(p,h){if(!D.isUndefined(p)){if(f.indexOf(p)!==-1)throw Error("Circular reference detected in "+h.join("."));f.push(p),D.forEach(p,function(m,S){(!(D.isUndefined(m)||m===null)&&o.call(t,m,D.isString(S)?S.trim():S,h,d))===!0&&v(m,h?h.concat(S):[S])}),f.pop()}}if(!D.isObject(e))throw new TypeError("data must be an object");return v(e),t}function Wp(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(r){return t[r]})}function Wc(e,t){this._pairs=[],e&&vl(e,this,t)}const ob=Wc.prototype;ob.append=function(t,n){this._pairs.push([t,n])};ob.toString=function(t){const n=t?function(r){return t.call(this,r,Wp)}:Wp;return this._pairs.map(function(o){return n(o[0])+"="+n(o[1])},"").join("&")};function NP(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function sb(e,t,n){if(!t)return e;const r=n&&n.encode||NP,o=D.isFunction(n)?{serialize:n}:n,s=o&&o.serialize;let i;if(s?i=s(t,o):i=D.isURLSearchParams(t)?t.toString():new Wc(t,o).toString(r),i){const a=e.indexOf("#");a!==-1&&(e=e.slice(0,a)),e+=(e.indexOf("?")===-1?"?":"&")+i}return e}class Gp{constructor(){this.handlers=[]}use(t,n,r){return this.handlers.push({fulfilled:t,rejected:n,synchronous:r?r.synchronous:!1,runWhen:r?r.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){D.forEach(this.handlers,function(r){r!==null&&t(r)})}}const Gc={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0},LP=typeof URLSearchParams<"u"?URLSearchParams:Wc,MP=typeof FormData<"u"?FormData:null,$P=typeof Blob<"u"?Blob:null,kP={isBrowser:!0,classes:{URLSearchParams:LP,FormData:MP,Blob:$P},protocols:["http","https","file","blob","url","data"]},Yc=typeof window<"u"&&typeof document<"u",Vu=typeof navigator=="object"&&navigator||void 0,FP=Yc&&(!Vu||["ReactNative","NativeScript","NS"].indexOf(Vu.product)<0),BP=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",DP=Yc&&window.location.href||"http://localhost",VP=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Yc,hasStandardBrowserEnv:FP,hasStandardBrowserWebWorkerEnv:BP,navigator:Vu,origin:DP},Symbol.toStringTag,{value:"Module"})),Nt={...VP,...kP};function jP(e,t){return vl(e,new Nt.classes.URLSearchParams,{visitor:function(n,r,o,s){return Nt.isNode&&D.isBuffer(n)?(this.append(r,n.toString("base64")),!1):s.defaultVisitor.apply(this,arguments)},...t})}function zP(e){return D.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function HP(e){const t={},n=Object.keys(e);let r;const o=n.length;let s;for(r=0;r=n.length;return i=!i&&D.isArray(o)?o.length:i,l?(D.hasOwnProp(o,i)?o[i]=[o[i],r]:o[i]=r,!a):((!o[i]||!D.isObject(o[i]))&&(o[i]=[]),t(n,r,o[i],s)&&D.isArray(o[i])&&(o[i]=HP(o[i])),!a)}if(D.isFormData(e)&&D.isFunction(e.entries)){const n={};return D.forEachEntry(e,(r,o)=>{t(zP(r),o,n,0)}),n}return null}function UP(e,t,n){if(D.isString(e))try{return(t||JSON.parse)(e),D.trim(e)}catch(r){if(r.name!=="SyntaxError")throw r}return(n||JSON.stringify)(e)}const yi={transitional:Gc,adapter:["xhr","http","fetch"],transformRequest:[function(t,n){const r=n.getContentType()||"",o=r.indexOf("application/json")>-1,s=D.isObject(t);if(s&&D.isHTMLForm(t)&&(t=new FormData(t)),D.isFormData(t))return o?JSON.stringify(ib(t)):t;if(D.isArrayBuffer(t)||D.isBuffer(t)||D.isStream(t)||D.isFile(t)||D.isBlob(t)||D.isReadableStream(t))return t;if(D.isArrayBufferView(t))return t.buffer;if(D.isURLSearchParams(t))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let a;if(s){if(r.indexOf("application/x-www-form-urlencoded")>-1)return jP(t,this.formSerializer).toString();if((a=D.isFileList(t))||r.indexOf("multipart/form-data")>-1){const l=this.env&&this.env.FormData;return vl(a?{"files[]":t}:t,l&&new l,this.formSerializer)}}return s||o?(n.setContentType("application/json",!1),UP(t)):t}],transformResponse:[function(t){const n=this.transitional||yi.transitional,r=n&&n.forcedJSONParsing,o=this.responseType==="json";if(D.isResponse(t)||D.isReadableStream(t))return t;if(t&&D.isString(t)&&(r&&!this.responseType||o)){const i=!(n&&n.silentJSONParsing)&&o;try{return JSON.parse(t,this.parseReviver)}catch(a){if(i)throw a.name==="SyntaxError"?Te.from(a,Te.ERR_BAD_RESPONSE,this,null,this.response):a}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:Nt.classes.FormData,Blob:Nt.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};D.forEach(["delete","get","head","post","put","patch"],e=>{yi.headers[e]={}});const KP=D.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),qP=e=>{const t={};let n,r,o;return e&&e.split(` +`).forEach(function(i){o=i.indexOf(":"),n=i.substring(0,o).trim().toLowerCase(),r=i.substring(o+1).trim(),!(!n||t[n]&&KP[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)}),t},Yp=Symbol("internals");function vs(e){return e&&String(e).trim().toLowerCase()}function oa(e){return e===!1||e==null?e:D.isArray(e)?e.map(oa):String(e)}function WP(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const GP=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function Wl(e,t,n,r,o){if(D.isFunction(r))return r.call(this,t,n);if(o&&(t=n),!!D.isString(t)){if(D.isString(r))return t.indexOf(r)!==-1;if(D.isRegExp(r))return r.test(t)}}function YP(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,r)=>n.toUpperCase()+r)}function JP(e,t){const n=D.toCamelCase(" "+t);["get","set","has"].forEach(r=>{Object.defineProperty(e,r+n,{value:function(o,s,i){return this[r].call(this,t,o,s,i)},configurable:!0})})}let Kt=class{constructor(t){t&&this.set(t)}set(t,n,r){const o=this;function s(a,l,u){const c=vs(l);if(!c)throw new Error("header name must be a non-empty string");const f=D.findKey(o,c);(!f||o[f]===void 0||u===!0||u===void 0&&o[f]!==!1)&&(o[f||l]=oa(a))}const i=(a,l)=>D.forEach(a,(u,c)=>s(u,c,l));if(D.isPlainObject(t)||t instanceof this.constructor)i(t,n);else if(D.isString(t)&&(t=t.trim())&&!GP(t))i(qP(t),n);else if(D.isObject(t)&&D.isIterable(t)){let a={},l,u;for(const c of t){if(!D.isArray(c))throw TypeError("Object iterator must return a key-value pair");a[u=c[0]]=(l=a[u])?D.isArray(l)?[...l,c[1]]:[l,c[1]]:c[1]}i(a,n)}else t!=null&&s(n,t,r);return this}get(t,n){if(t=vs(t),t){const r=D.findKey(this,t);if(r){const o=this[r];if(!n)return o;if(n===!0)return WP(o);if(D.isFunction(n))return n.call(this,o,r);if(D.isRegExp(n))return n.exec(o);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=vs(t),t){const r=D.findKey(this,t);return!!(r&&this[r]!==void 0&&(!n||Wl(this,this[r],r,n)))}return!1}delete(t,n){const r=this;let o=!1;function s(i){if(i=vs(i),i){const a=D.findKey(r,i);a&&(!n||Wl(r,r[a],a,n))&&(delete r[a],o=!0)}}return D.isArray(t)?t.forEach(s):s(t),o}clear(t){const n=Object.keys(this);let r=n.length,o=!1;for(;r--;){const s=n[r];(!t||Wl(this,this[s],s,t,!0))&&(delete this[s],o=!0)}return o}normalize(t){const n=this,r={};return D.forEach(this,(o,s)=>{const i=D.findKey(r,s);if(i){n[i]=oa(o),delete n[s];return}const a=t?YP(s):String(s).trim();a!==s&&delete n[s],n[a]=oa(o),r[a]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return D.forEach(this,(r,o)=>{r!=null&&r!==!1&&(n[o]=t&&D.isArray(r)?r.join(", "):r)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(` +`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const r=new this(t);return n.forEach(o=>r.set(o)),r}static accessor(t){const r=(this[Yp]=this[Yp]={accessors:{}}).accessors,o=this.prototype;function s(i){const a=vs(i);r[a]||(JP(o,i),r[a]=!0)}return D.isArray(t)?t.forEach(s):s(t),this}};Kt.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);D.reduceDescriptors(Kt.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(r){this[n]=r}}});D.freezeMethods(Kt);function Gl(e,t){const n=this||yi,r=t||n,o=Kt.from(r.headers);let s=r.data;return D.forEach(e,function(a){s=a.call(n,s,o.normalize(),t?t.status:void 0)}),o.normalize(),s}function ab(e){return!!(e&&e.__CANCEL__)}let wi=class extends Te{constructor(t,n,r){super(t??"canceled",Te.ERR_CANCELED,n,r),this.name="CanceledError",this.__CANCEL__=!0}};function lb(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new Te("Request failed with status code "+n.status,[Te.ERR_BAD_REQUEST,Te.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function XP(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function ZP(e,t){e=e||10;const n=new Array(e),r=new Array(e);let o=0,s=0,i;return t=t!==void 0?t:1e3,function(l){const u=Date.now(),c=r[s];i||(i=u),n[o]=l,r[o]=u;let f=s,d=0;for(;f!==o;)d+=n[f++],f=f%e;if(o=(o+1)%e,o===s&&(s=(s+1)%e),u-i{n=c,o=null,s&&(clearTimeout(s),s=null),e(...u)};return[(...u)=>{const c=Date.now(),f=c-n;f>=r?i(u,c):(o=u,s||(s=setTimeout(()=>{s=null,i(o)},r-f)))},()=>o&&i(o)]}const Pa=(e,t,n=3)=>{let r=0;const o=ZP(50,250);return QP(s=>{const i=s.loaded,a=s.lengthComputable?s.total:void 0,l=i-r,u=o(l),c=i<=a;r=i;const f={loaded:i,total:a,progress:a?i/a:void 0,bytes:l,rate:u||void 0,estimated:u&&a&&c?(a-i)/u:void 0,event:s,lengthComputable:a!=null,[t?"download":"upload"]:!0};e(f)},n)},Jp=(e,t)=>{const n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},Xp=e=>(...t)=>D.asap(()=>e(...t)),e8=Nt.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,Nt.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(Nt.origin),Nt.navigator&&/(msie|trident)/i.test(Nt.navigator.userAgent)):()=>!0,t8=Nt.hasStandardBrowserEnv?{write(e,t,n,r,o,s,i){if(typeof document>"u")return;const a=[`${e}=${encodeURIComponent(t)}`];D.isNumber(n)&&a.push(`expires=${new Date(n).toUTCString()}`),D.isString(r)&&a.push(`path=${r}`),D.isString(o)&&a.push(`domain=${o}`),s===!0&&a.push("secure"),D.isString(i)&&a.push(`SameSite=${i}`),document.cookie=a.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.match(new RegExp("(?:^|; )"+e+"=([^;]*)"));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function n8(e){return typeof e!="string"?!1:/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function r8(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function ub(e,t,n){let r=!n8(t);return e&&(r||n==!1)?r8(e,t):t}const Zp=e=>e instanceof Kt?{...e}:e;function vo(e,t){t=t||{};const n={};function r(u,c,f,d){return D.isPlainObject(u)&&D.isPlainObject(c)?D.merge.call({caseless:d},u,c):D.isPlainObject(c)?D.merge({},c):D.isArray(c)?c.slice():c}function o(u,c,f,d){if(D.isUndefined(c)){if(!D.isUndefined(u))return r(void 0,u,f,d)}else return r(u,c,f,d)}function s(u,c){if(!D.isUndefined(c))return r(void 0,c)}function i(u,c){if(D.isUndefined(c)){if(!D.isUndefined(u))return r(void 0,u)}else return r(void 0,c)}function a(u,c,f){if(f in t)return r(u,c);if(f in e)return r(void 0,u)}const l={url:s,method:s,data:s,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:a,headers:(u,c,f)=>o(Zp(u),Zp(c),f,!0)};return D.forEach(Object.keys({...e,...t}),function(c){if(c==="__proto__"||c==="constructor"||c==="prototype")return;const f=D.hasOwnProp(l,c)?l[c]:o,d=f(e[c],t[c],c);D.isUndefined(d)&&f!==a||(n[c]=d)}),n}const cb=e=>{const t=vo({},e);let{data:n,withXSRFToken:r,xsrfHeaderName:o,xsrfCookieName:s,headers:i,auth:a}=t;if(t.headers=i=Kt.from(i),t.url=sb(ub(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),a&&i.set("Authorization","Basic "+btoa((a.username||"")+":"+(a.password?unescape(encodeURIComponent(a.password)):""))),D.isFormData(n)){if(Nt.hasStandardBrowserEnv||Nt.hasStandardBrowserWebWorkerEnv)i.setContentType(void 0);else if(D.isFunction(n.getHeaders)){const l=n.getHeaders(),u=["content-type","content-length"];Object.entries(l).forEach(([c,f])=>{u.includes(c.toLowerCase())&&i.set(c,f)})}}if(Nt.hasStandardBrowserEnv&&(r&&D.isFunction(r)&&(r=r(t)),r||r!==!1&&e8(t.url))){const l=o&&s&&t8.read(s);l&&i.set(o,l)}return t},o8=typeof XMLHttpRequest<"u",s8=o8&&function(e){return new Promise(function(n,r){const o=cb(e);let s=o.data;const i=Kt.from(o.headers).normalize();let{responseType:a,onUploadProgress:l,onDownloadProgress:u}=o,c,f,d,v,p;function h(){v&&v(),p&&p(),o.cancelToken&&o.cancelToken.unsubscribe(c),o.signal&&o.signal.removeEventListener("abort",c)}let b=new XMLHttpRequest;b.open(o.method.toUpperCase(),o.url,!0),b.timeout=o.timeout;function m(){if(!b)return;const _=Kt.from("getAllResponseHeaders"in b&&b.getAllResponseHeaders()),y={data:!a||a==="text"||a==="json"?b.responseText:b.response,status:b.status,statusText:b.statusText,headers:_,config:e,request:b};lb(function(R){n(R),h()},function(R){r(R),h()},y),b=null}"onloadend"in b?b.onloadend=m:b.onreadystatechange=function(){!b||b.readyState!==4||b.status===0&&!(b.responseURL&&b.responseURL.indexOf("file:")===0)||setTimeout(m)},b.onabort=function(){b&&(r(new Te("Request aborted",Te.ECONNABORTED,e,b)),b=null)},b.onerror=function(w){const y=w&&w.message?w.message:"Network Error",A=new Te(y,Te.ERR_NETWORK,e,b);A.event=w||null,r(A),b=null},b.ontimeout=function(){let w=o.timeout?"timeout of "+o.timeout+"ms exceeded":"timeout exceeded";const y=o.transitional||Gc;o.timeoutErrorMessage&&(w=o.timeoutErrorMessage),r(new Te(w,y.clarifyTimeoutError?Te.ETIMEDOUT:Te.ECONNABORTED,e,b)),b=null},s===void 0&&i.setContentType(null),"setRequestHeader"in b&&D.forEach(i.toJSON(),function(w,y){b.setRequestHeader(y,w)}),D.isUndefined(o.withCredentials)||(b.withCredentials=!!o.withCredentials),a&&a!=="json"&&(b.responseType=o.responseType),u&&([d,p]=Pa(u,!0),b.addEventListener("progress",d)),l&&b.upload&&([f,v]=Pa(l),b.upload.addEventListener("progress",f),b.upload.addEventListener("loadend",v)),(o.cancelToken||o.signal)&&(c=_=>{b&&(r(!_||_.type?new wi(null,e,b):_),b.abort(),b=null)},o.cancelToken&&o.cancelToken.subscribe(c),o.signal&&(o.signal.aborted?c():o.signal.addEventListener("abort",c)));const S=XP(o.url);if(S&&Nt.protocols.indexOf(S)===-1){r(new Te("Unsupported protocol "+S+":",Te.ERR_BAD_REQUEST,e));return}b.send(s||null)})},i8=(e,t)=>{const{length:n}=e=e?e.filter(Boolean):[];if(t||n){let r=new AbortController,o;const s=function(u){if(!o){o=!0,a();const c=u instanceof Error?u:this.reason;r.abort(c instanceof Te?c:new wi(c instanceof Error?c.message:c))}};let i=t&&setTimeout(()=>{i=null,s(new Te(`timeout of ${t}ms exceeded`,Te.ETIMEDOUT))},t);const a=()=>{e&&(i&&clearTimeout(i),i=null,e.forEach(u=>{u.unsubscribe?u.unsubscribe(s):u.removeEventListener("abort",s)}),e=null)};e.forEach(u=>u.addEventListener("abort",s));const{signal:l}=r;return l.unsubscribe=()=>D.asap(a),l}},a8=function*(e,t){let n=e.byteLength;if(n{const o=l8(e,t);let s=0,i,a=l=>{i||(i=!0,r&&r(l))};return new ReadableStream({async pull(l){try{const{done:u,value:c}=await o.next();if(u){a(),l.close();return}let f=c.byteLength;if(n){let d=s+=f;n(d)}l.enqueue(new Uint8Array(c))}catch(u){throw a(u),u}},cancel(l){return a(l),o.return()}},{highWaterMark:2})},eh=64*1024,{isFunction:Di}=D,c8=(({Request:e,Response:t})=>({Request:e,Response:t}))(D.global),{ReadableStream:th,TextEncoder:nh}=D.global,rh=(e,...t)=>{try{return!!e(...t)}catch{return!1}},f8=e=>{e=D.merge.call({skipUndefined:!0},c8,e);const{fetch:t,Request:n,Response:r}=e,o=t?Di(t):typeof fetch=="function",s=Di(n),i=Di(r);if(!o)return!1;const a=o&&Di(th),l=o&&(typeof nh=="function"?(p=>h=>p.encode(h))(new nh):async p=>new Uint8Array(await new n(p).arrayBuffer())),u=s&&a&&rh(()=>{let p=!1;const h=new n(Nt.origin,{body:new th,method:"POST",get duplex(){return p=!0,"half"}}).headers.has("Content-Type");return p&&!h}),c=i&&a&&rh(()=>D.isReadableStream(new r("").body)),f={stream:c&&(p=>p.body)};o&&["text","arrayBuffer","blob","formData","stream"].forEach(p=>{!f[p]&&(f[p]=(h,b)=>{let m=h&&h[p];if(m)return m.call(h);throw new Te(`Response type '${p}' is not supported`,Te.ERR_NOT_SUPPORT,b)})});const d=async p=>{if(p==null)return 0;if(D.isBlob(p))return p.size;if(D.isSpecCompliantForm(p))return(await new n(Nt.origin,{method:"POST",body:p}).arrayBuffer()).byteLength;if(D.isArrayBufferView(p)||D.isArrayBuffer(p))return p.byteLength;if(D.isURLSearchParams(p)&&(p=p+""),D.isString(p))return(await l(p)).byteLength},v=async(p,h)=>{const b=D.toFiniteNumber(p.getContentLength());return b??d(h)};return async p=>{let{url:h,method:b,data:m,signal:S,cancelToken:_,timeout:w,onDownloadProgress:y,onUploadProgress:A,responseType:R,headers:N,withCredentials:I="same-origin",fetchOptions:x}=cb(p),k=t||fetch;R=R?(R+"").toLowerCase():"text";let $=i8([S,_&&_.toAbortSignal()],w),F=null;const Y=$&&$.unsubscribe&&(()=>{$.unsubscribe()});let P;try{if(A&&u&&b!=="get"&&b!=="head"&&(P=await v(N,m))!==0){let Re=new n(h,{method:"POST",body:m,duplex:"half"}),Ce;if(D.isFormData(m)&&(Ce=Re.headers.get("content-type"))&&N.setContentType(Ce),Re.body){const[_e,qe]=Jp(P,Pa(Xp(A)));m=Qp(Re.body,eh,_e,qe)}}D.isString(I)||(I=I?"include":"omit");const O=s&&"credentials"in n.prototype,j={...x,signal:$,method:b.toUpperCase(),headers:N.normalize().toJSON(),body:m,duplex:"half",credentials:O?I:void 0};F=s&&new n(h,j);let Q=await(s?k(F,x):k(h,j));const me=c&&(R==="stream"||R==="response");if(c&&(y||me&&Y)){const Re={};["status","statusText","headers"].forEach(ze=>{Re[ze]=Q[ze]});const Ce=D.toFiniteNumber(Q.headers.get("content-length")),[_e,qe]=y&&Jp(Ce,Pa(Xp(y),!0))||[];Q=new r(Qp(Q.body,eh,_e,()=>{qe&&qe(),Y&&Y()}),Re)}R=R||"text";let Pe=await f[D.findKey(f,R)||"text"](Q,p);return!me&&Y&&Y(),await new Promise((Re,Ce)=>{lb(Re,Ce,{data:Pe,headers:Kt.from(Q.headers),status:Q.status,statusText:Q.statusText,config:p,request:F})})}catch(O){throw Y&&Y(),O&&O.name==="TypeError"&&/Load failed|fetch/i.test(O.message)?Object.assign(new Te("Network Error",Te.ERR_NETWORK,p,F,O&&O.response),{cause:O.cause||O}):Te.from(O,O&&O.code,p,F,O&&O.response)}}},d8=new Map,fb=e=>{let t=e&&e.env||{};const{fetch:n,Request:r,Response:o}=t,s=[r,o,n];let i=s.length,a=i,l,u,c=d8;for(;a--;)l=s[a],u=c.get(l),u===void 0&&c.set(l,u=a?new Map:f8(t)),c=u;return u};fb();const Jc={http:xP,xhr:s8,fetch:{get:fb}};D.forEach(Jc,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const oh=e=>`- ${e}`,p8=e=>D.isFunction(e)||e===null||e===!1;function h8(e,t){e=D.isArray(e)?e:[e];const{length:n}=e;let r,o;const s={};for(let i=0;i`adapter ${l} `+(u===!1?"is not supported by the environment":"is not available in the build"));let a=n?i.length>1?`since : +`+i.map(oh).join(` +`):" "+oh(i[0]):"as no adapter specified";throw new Te("There is no suitable adapter to dispatch the request "+a,"ERR_NOT_SUPPORT")}return o}const db={getAdapter:h8,adapters:Jc};function Yl(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new wi(null,e)}function sh(e){return Yl(e),e.headers=Kt.from(e.headers),e.data=Gl.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),db.getAdapter(e.adapter||yi.adapter,e)(e).then(function(r){return Yl(e),r.data=Gl.call(e,e.transformResponse,r),r.headers=Kt.from(r.headers),r},function(r){return ab(r)||(Yl(e),r&&r.response&&(r.response.data=Gl.call(e,e.transformResponse,r.response),r.response.headers=Kt.from(r.response.headers))),Promise.reject(r)})}const pb="1.13.6",ml={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{ml[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const ih={};ml.transitional=function(t,n,r){function o(s,i){return"[Axios v"+pb+"] Transitional option '"+s+"'"+i+(r?". "+r:"")}return(s,i,a)=>{if(t===!1)throw new Te(o(i," has been removed"+(n?" in "+n:"")),Te.ERR_DEPRECATED);return n&&!ih[i]&&(ih[i]=!0),t?t(s,i,a):!0}};ml.spelling=function(t){return(n,r)=>!0};function v8(e,t,n){if(typeof e!="object")throw new Te("options must be an object",Te.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let o=r.length;for(;o-- >0;){const s=r[o],i=t[s];if(i){const a=e[s],l=a===void 0||i(a,s,e);if(l!==!0)throw new Te("option "+s+" must be "+l,Te.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new Te("Unknown option "+s,Te.ERR_BAD_OPTION)}}const sa={assertOptions:v8,validators:ml},an=sa.validators;let lo=class{constructor(t){this.defaults=t||{},this.interceptors={request:new Gp,response:new Gp}}async request(t,n){try{return await this._request(t,n)}catch(r){if(r instanceof Error){let o={};Error.captureStackTrace?Error.captureStackTrace(o):o=new Error;const s=o.stack?o.stack.replace(/^.+\n/,""):"";try{r.stack?s&&!String(r.stack).endsWith(s.replace(/^.+\n.+\n/,""))&&(r.stack+=` +`+s):r.stack=s}catch{}}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=vo(this.defaults,n);const{transitional:r,paramsSerializer:o,headers:s}=n;r!==void 0&&sa.assertOptions(r,{silentJSONParsing:an.transitional(an.boolean),forcedJSONParsing:an.transitional(an.boolean),clarifyTimeoutError:an.transitional(an.boolean),legacyInterceptorReqResOrdering:an.transitional(an.boolean)},!1),o!=null&&(D.isFunction(o)?n.paramsSerializer={serialize:o}:sa.assertOptions(o,{encode:an.function,serialize:an.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),sa.assertOptions(n,{baseUrl:an.spelling("baseURL"),withXsrfToken:an.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let i=s&&D.merge(s.common,s[n.method]);s&&D.forEach(["delete","get","head","post","put","patch","common"],p=>{delete s[p]}),n.headers=Kt.concat(i,s);const a=[];let l=!0;this.interceptors.request.forEach(function(h){if(typeof h.runWhen=="function"&&h.runWhen(n)===!1)return;l=l&&h.synchronous;const b=n.transitional||Gc;b&&b.legacyInterceptorReqResOrdering?a.unshift(h.fulfilled,h.rejected):a.push(h.fulfilled,h.rejected)});const u=[];this.interceptors.response.forEach(function(h){u.push(h.fulfilled,h.rejected)});let c,f=0,d;if(!l){const p=[sh.bind(this),void 0];for(p.unshift(...a),p.push(...u),d=p.length,c=Promise.resolve(n);f{if(!r._listeners)return;let s=r._listeners.length;for(;s-- >0;)r._listeners[s](o);r._listeners=null}),this.promise.then=o=>{let s;const i=new Promise(a=>{r.subscribe(a),s=a}).then(o);return i.cancel=function(){r.unsubscribe(s)},i},t(function(s,i,a){r.reason||(r.reason=new wi(s,i,a),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const t=new AbortController,n=r=>{t.abort(r)};return this.subscribe(n),t.signal.unsubscribe=()=>this.unsubscribe(n),t.signal}static source(){let t;return{token:new hb(function(o){t=o}),cancel:t}}};function g8(e){return function(n){return e.apply(null,n)}}function b8(e){return D.isObject(e)&&e.isAxiosError===!0}const ju={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(ju).forEach(([e,t])=>{ju[t]=e});function vb(e){const t=new lo(e),n=Gg(lo.prototype.request,t);return D.extend(n,lo.prototype,t,{allOwnKeys:!0}),D.extend(n,t,null,{allOwnKeys:!0}),n.create=function(o){return vb(vo(e,o))},n}const mt=vb(yi);mt.Axios=lo;mt.CanceledError=wi;mt.CancelToken=m8;mt.isCancel=ab;mt.VERSION=pb;mt.toFormData=vl;mt.AxiosError=Te;mt.Cancel=mt.CanceledError;mt.all=function(t){return Promise.all(t)};mt.spread=g8;mt.isAxiosError=b8;mt.mergeConfig=vo;mt.AxiosHeaders=Kt;mt.formToJSON=e=>ib(D.isHTMLForm(e)?new FormData(e):e);mt.getAdapter=db.getAdapter;mt.HttpStatusCode=ju;mt.default=mt;const{Axios:JN,AxiosError:XN,CanceledError:ZN,isCancel:QN,CancelToken:eL,VERSION:tL,all:nL,Cancel:rL,isAxiosError:oL,spread:sL,toFormData:iL,AxiosHeaders:aL,HttpStatusCode:lL,formToJSON:uL,getAdapter:cL,mergeConfig:fL}=mt;function mb(e){return Fa()?(Ba(e),!0):!1}function ln(e){return typeof e=="function"?e():g(e)}const y8=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const w8=Object.prototype.toString,S8=e=>w8.call(e)==="[object Object]",Jo=()=>{};function Xc(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}const gb=e=>e();function E8(e,t={}){let n,r,o=Jo;const s=a=>{clearTimeout(a),o(),o=Jo};return a=>{const l=ln(e),u=ln(t.maxWait);return n&&s(n),l<=0||u!==void 0&&u<=0?(r&&(s(r),r=null),Promise.resolve(a())):new Promise((c,f)=>{o=t.rejectOnCancel?f:c,u&&!r&&(r=setTimeout(()=>{n&&s(n),r=null,c(a())},u)),n=setTimeout(()=>{r&&s(r),r=null,c(a())},l)})}}function _8(...e){let t=0,n,r=!0,o=Jo,s,i,a,l,u;!Ue(e[0])&&typeof e[0]=="object"?{delay:i,trailing:a=!0,leading:l=!0,rejectOnCancel:u=!1}=e[0]:[i,a=!0,l=!0,u=!1]=e;const c=()=>{n&&(clearTimeout(n),n=void 0,o(),o=Jo)};return d=>{const v=ln(i),p=Date.now()-t,h=()=>s=d();return c(),v<=0?(t=Date.now(),h()):(p>v&&(l||!r)?(t=Date.now(),h()):a&&(s=new Promise((b,m)=>{o=u?m:b,n=setTimeout(()=>{t=Date.now(),r=!0,b(h()),c()},Math.max(0,v-p))})),!l&&!n&&(n=setTimeout(()=>r=!0,v)),r=!1,s)}}function C8(e=gb){const t=V(!0);function n(){t.value=!1}function r(){t.value=!0}return{isActive:Mr(t),pause:n,resume:r,eventFilter:(...s)=>{t.value&&e(...s)}}}function T8(e){return Ge()}function O8(...e){if(e.length!==1)return Jt(...e);const t=e[0];return typeof t=="function"?Mr(Ay(()=>({get:t,set:Jo}))):V(t)}function dL(e,t=200,n={}){return Xc(E8(t,n),e)}function pL(e,t=200,n=!1,r=!0,o=!1){return Xc(_8(t,n,r,o),e)}function A8(e,t,n={}){const{eventFilter:r=gb,...o}=n;return he(e,Xc(r,t),o)}function R8(e,t,n={}){const{eventFilter:r,...o}=n,{eventFilter:s,pause:i,resume:a,isActive:l}=C8(r);return{stop:A8(e,t,{...o,eventFilter:s}),pause:i,resume:a,isActive:l}}function bb(e,t=!0,n){T8()?Ke(e,n):t?e():Ie(e)}const x8=/[YMDHhms]o|\[([^\]]+)\]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a{1,2}|A{1,2}|m{1,2}|s{1,2}|Z{1,2}|SSS/g;function I8(e,t,n,r){let o=e<12?"AM":"PM";return r&&(o=o.split("").reduce((s,i)=>s+=`${i}.`,"")),n?o.toLowerCase():o}function Gr(e){const t=["th","st","nd","rd"],n=e%100;return e+(t[(n-20)%10]||t[n]||t[0])}function hL(e,t,n={}){var r;const o=e.getFullYear(),s=e.getMonth(),i=e.getDate(),a=e.getHours(),l=e.getMinutes(),u=e.getSeconds(),c=e.getMilliseconds(),f=e.getDay(),d=(r=n.customMeridiem)!=null?r:I8,v={Yo:()=>Gr(o),YY:()=>String(o).slice(-2),YYYY:()=>o,M:()=>s+1,Mo:()=>Gr(s+1),MM:()=>`${s+1}`.padStart(2,"0"),MMM:()=>e.toLocaleDateString(ln(n.locales),{month:"short"}),MMMM:()=>e.toLocaleDateString(ln(n.locales),{month:"long"}),D:()=>String(i),Do:()=>Gr(i),DD:()=>`${i}`.padStart(2,"0"),H:()=>String(a),Ho:()=>Gr(a),HH:()=>`${a}`.padStart(2,"0"),h:()=>`${a%12||12}`.padStart(1,"0"),ho:()=>Gr(a%12||12),hh:()=>`${a%12||12}`.padStart(2,"0"),m:()=>String(l),mo:()=>Gr(l),mm:()=>`${l}`.padStart(2,"0"),s:()=>String(u),so:()=>Gr(u),ss:()=>`${u}`.padStart(2,"0"),SSS:()=>`${c}`.padStart(3,"0"),d:()=>f,dd:()=>e.toLocaleDateString(ln(n.locales),{weekday:"narrow"}),ddd:()=>e.toLocaleDateString(ln(n.locales),{weekday:"short"}),dddd:()=>e.toLocaleDateString(ln(n.locales),{weekday:"long"}),A:()=>d(a,l),AA:()=>d(a,l,!1,!0),a:()=>d(a,l,!0),aa:()=>d(a,l,!0,!0)};return t.replace(x8,(p,h)=>{var b,m;return(m=h??((b=v[p])==null?void 0:b.call(v)))!=null?m:p})}/*! + * pinia v2.3.1 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let yb;const gl=e=>yb=e,wb=Symbol();function zu(e){return e&&typeof e=="object"&&Object.prototype.toString.call(e)==="[object Object]"&&typeof e.toJSON!="function"}var Ms;(function(e){e.direct="direct",e.patchObject="patch object",e.patchFunction="patch function"})(Ms||(Ms={}));function vL(){const e=Th(!0),t=e.run(()=>V({}));let n=[],r=[];const o=Ds({install(s){gl(o),o._a=s,s.provide(wb,o),s.config.globalProperties.$pinia=o,r.forEach(i=>n.push(i)),r=[]},use(s){return this._a?n.push(s):r.push(s),this},_p:n,_a:null,_e:e,_s:new Map,state:t});return o}const Sb=()=>{};function ah(e,t,n,r=Sb){e.push(t);const o=()=>{const s=e.indexOf(t);s>-1&&(e.splice(s,1),r())};return!n&&Fa()&&Ba(o),o}function Ao(e,...t){e.slice().forEach(n=>{n(...t)})}const P8=e=>e(),lh=Symbol(),Jl=Symbol();function Hu(e,t){e instanceof Map&&t instanceof Map?t.forEach((n,r)=>e.set(r,n)):e instanceof Set&&t instanceof Set&&t.forEach(e.add,e);for(const n in t){if(!t.hasOwnProperty(n))continue;const r=t[n],o=e[n];zu(o)&&zu(r)&&e.hasOwnProperty(n)&&!Ue(r)&&!Hn(r)?e[n]=Hu(o,r):e[n]=r}return e}const N8=Symbol();function L8(e){return!zu(e)||!e.hasOwnProperty(N8)}const{assign:Rr}=Object;function M8(e){return!!(Ue(e)&&e.effect)}function $8(e,t,n,r){const{state:o,actions:s,getters:i}=t,a=n.state.value[e];let l;function u(){a||(n.state.value[e]=o?o():{});const c=vr(n.state.value[e]);return Rr(c,s,Object.keys(i||{}).reduce((f,d)=>(f[d]=Ds(C(()=>{gl(n);const v=n._s.get(e);return i[d].call(v,v)})),f),{}))}return l=Eb(e,u,t,n,r,!0),l}function Eb(e,t,n={},r,o,s){let i;const a=Rr({actions:{}},n),l={deep:!0};let u,c,f=[],d=[],v;const p=r.state.value[e];!s&&!p&&(r.state.value[e]={});let h;function b(N){let I;u=c=!1,typeof N=="function"?(N(r.state.value[e]),I={type:Ms.patchFunction,storeId:e,events:v}):(Hu(r.state.value[e],N),I={type:Ms.patchObject,payload:N,storeId:e,events:v});const x=h=Symbol();Ie().then(()=>{h===x&&(u=!0)}),c=!0,Ao(f,I,r.state.value[e])}const m=s?function(){const{state:I}=n,x=I?I():{};this.$patch(k=>{Rr(k,x)})}:Sb;function S(){i.stop(),f=[],d=[],r._s.delete(e)}const _=(N,I="")=>{if(lh in N)return N[Jl]=I,N;const x=function(){gl(r);const k=Array.from(arguments),$=[],F=[];function Y(j){$.push(j)}function P(j){F.push(j)}Ao(d,{args:k,name:x[Jl],store:y,after:Y,onError:P});let O;try{O=N.apply(this&&this.$id===e?this:y,k)}catch(j){throw Ao(F,j),j}return O instanceof Promise?O.then(j=>(Ao($,j),j)).catch(j=>(Ao(F,j),Promise.reject(j))):(Ao($,O),O)};return x[lh]=!0,x[Jl]=I,x},w={_p:r,$id:e,$onAction:ah.bind(null,d),$patch:b,$reset:m,$subscribe(N,I={}){const x=ah(f,N,I.detached,()=>k()),k=i.run(()=>he(()=>r.state.value[e],$=>{(I.flush==="sync"?c:u)&&N({storeId:e,type:Ms.direct,events:v},$)},Rr({},l,I)));return x},$dispose:S},y=wt(w);r._s.set(e,y);const R=(r._a&&r._a.runWithContext||P8)(()=>r._e.run(()=>(i=Th()).run(()=>t({action:_}))));for(const N in R){const I=R[N];if(Ue(I)&&!M8(I)||Hn(I))s||(p&&L8(I)&&(Ue(I)?I.value=p[N]:Hu(I,p[N])),r.state.value[e][N]=I);else if(typeof I=="function"){const x=_(I,N);R[N]=x,a.actions[N]=I}}return Rr(y,R),Rr(Me(y),R),Object.defineProperty(y,"$state",{get:()=>r.state.value[e],set:N=>{b(I=>{Rr(I,N)})}}),r._p.forEach(N=>{Rr(y,i.run(()=>N({store:y,app:r._a,pinia:r,options:a})))}),p&&s&&n.hydrate&&n.hydrate(y.$state,p),u=!0,c=!0,y}/*! #__NO_SIDE_EFFECTS__ */function mL(e,t,n){let r,o;const s=typeof t=="function";typeof e=="string"?(r=e,o=s?n:t):(o=e,r=e.id);function i(a,l){const u=ky();return a=a||(u?Ee(wb,null):null),a&&gl(a),a=yb,a._s.has(r)||(s?Eb(r,t,o,a):$8(r,o,a)),a._s.get(r)}return i.$id=r,i}function gL(e){{const t=Me(e),n={};for(const r in t){const o=t[r];o.effect?n[r]=C({get:()=>e[r],set(s){e[r]=s}}):(Ue(o)||Hn(o))&&(n[r]=Jt(e,r))}return n}}function uh(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function Vi(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);nthis.range.start)){var r=Math.max(n-this.param.buffer,0);this.checkRange(r,this.getEndByStart(r))}}},{key:"handleBehind",value:function(){var n=this.getScrollOvers();nn&&(i=o-1)}return r>0?--r:0}},{key:"getIndexOffset",value:function(n){if(!n)return 0;for(var r=0,o=0,s=0;s=O&&r("tobottom")},m=function(Y){var P=v(),O=p(),j=h();P<0||P+O>j+1||!j||(f.handleScroll(P),b(P,O,j,Y))},S=function(){var Y=t.dataKey,P=t.dataSources,O=P===void 0?[]:P;return O.map(function(j){return typeof Y=="function"?Y(j):j[Y]})},_=function(Y){l.value=Y},w=function(){f=new K8({slotHeaderSize:0,slotFooterSize:0,keeps:t.keeps,estimateSize:t.estimateSize,buffer:Math.round(t.keeps/3),uniqueIds:S()},_),l.value=f.getRange()},y=function(Y){if(Y>=t.dataSources.length-1)x();else{var P=f.getOffset(Y);A(P)}},A=function(Y){t.pageMode?(document.body[a]=Y,document.documentElement[a]=Y):u.value&&(u.value[a]=Y)},R=function(){for(var Y=[],P=l.value,O=P.start,j=P.end,Q=t.dataSources,me=t.dataKey,Pe=t.itemClass,Re=t.itemTag,Ce=t.itemStyle,_e=t.extraProps,qe=t.dataComponent,ze=t.itemScopedSlots,De=O;De<=j;De++){var B=Q[De];if(B){var K=typeof me=="function"?me(B):B[me];(typeof K=="string"||typeof K=="number")&&Y.push(re(Y8,{index:De,tag:Re,event:$s.ITEM,horizontal:i,uniqueKey:K,source:B,extraProps:_e,component:qe,scopedSlots:ze,style:Ce,class:"".concat(Pe).concat(t.itemClassAdd?" "+t.itemClassAdd(De):""),onItemResize:N},null))}}return Y},N=function(Y,P){f.saveSize(Y,P),r("resized",Y,P)},I=function(Y,P,O){Y===Lo.HEADER?f.updateParam("slotHeaderSize",P):Y===Lo.FOOTER&&f.updateParam("slotFooterSize",P),O&&f.handleSlotSizeChange()},x=function F(){if(c.value){var Y=c.value[i?"offsetLeft":"offsetTop"];A(Y),setTimeout(function(){v()+p()0:!1;function Zl(e,t,n,r){e.addEventListener?e.addEventListener(t,n,r):e.attachEvent&&e.attachEvent("on".concat(t),n)}function ms(e,t,n,r){e.removeEventListener?e.removeEventListener(t,n,r):e.detachEvent&&e.detachEvent("on".concat(t),n)}function Cb(e,t){const n=t.slice(0,t.length-1);for(let r=0;r=0;)t[n-1]+=",",t.splice(n,1),n=t.lastIndexOf("");return t}function J8(e,t){const n=e.length>=t.length?e:t,r=e.length>=t.length?t:e;let o=!0;for(let s=0;sni[e.toLowerCase()]||Cn[e.toLowerCase()]||e.toUpperCase().charCodeAt(0),X8=e=>Object.keys(ni).find(t=>ni[t]===e),Z8=e=>Object.keys(Cn).find(t=>Cn[t]===e);function Ab(e){Ob=e||"all"}function ri(){return Ob||"all"}function Q8(){return at.slice(0)}function eN(){return at.map(e=>X8(e)||Z8(e)||String.fromCharCode(e))}function tN(){const e=[];return Object.keys(lt).forEach(t=>{lt[t].forEach(n=>{let{key:r,scope:o,mods:s,shortcut:i}=n;e.push({scope:o,shortcut:i,mods:s,keys:r.split("+").map(a=>is(a))})})}),e}function nN(e){const t=e.target||e.srcElement,{tagName:n}=t;let r=!0;const o=n==="INPUT"&&!["checkbox","radio","range","button","file","reset","submit","color"].includes(t.type);return(t.isContentEditable||(o||n==="TEXTAREA"||n==="SELECT")&&!t.readOnly)&&(r=!1),r}function rN(e){return typeof e=="string"&&(e=is(e)),at.indexOf(e)!==-1}function oN(e,t){let n,r;e||(e=ri());for(const o in lt)if(Object.prototype.hasOwnProperty.call(lt,o))for(n=lt[o],r=0;r{let{element:a}=i;return Zc(a)}):r++;ri()===e&&Ab(t||"all")}function sN(e){let t=e.keyCode||e.which||e.charCode;e.key&&e.key.toLowerCase()==="capslock"&&(t=is(e.key));const n=at.indexOf(t);if(n>=0&&at.splice(n,1),e.key&&e.key.toLowerCase()==="meta"&&at.splice(0,at.length),(t===93||t===224)&&(t=91),t in Et){Et[t]=!1;for(const r in Cn)Cn[r]===t&&(Lr[r]=!1)}}function Rb(e){if(typeof e>"u")Object.keys(lt).forEach(o=>{Array.isArray(lt[o])&<[o].forEach(s=>ji(s)),delete lt[o]}),Zc(null);else if(Array.isArray(e))e.forEach(o=>{o.key&&ji(o)});else if(typeof e=="object")e.key&&ji(e);else if(typeof e=="string"){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{let{key:t,scope:n,method:r,splitKey:o="+"}=e;Tb(t).forEach(i=>{const a=i.split(o),l=a.length,u=a[l-1],c=u==="*"?"*":is(u);if(!lt[c])return;n||(n=ri());const f=l>1?Cb(Cn,a):[],d=[];lt[c]=lt[c].filter(v=>{const h=(r?v.method===r:!0)&&v.scope===n&&J8(v.mods,f);return h&&d.push(v.element),!h}),d.forEach(v=>Zc(v))})};function dh(e,t,n,r){if(t.element!==r)return;let o;if(t.scope===n||t.scope==="all"){o=t.mods.length>0;for(const s in Et)Object.prototype.hasOwnProperty.call(Et,s)&&(!Et[s]&&t.mods.indexOf(+s)>-1||Et[s]&&t.mods.indexOf(+s)===-1)&&(o=!1);(t.mods.length===0&&!Et[16]&&!Et[18]&&!Et[17]&&!Et[91]||o||t.shortcut==="*")&&(t.keys=[],t.keys=t.keys.concat(at),t.method(e,t)===!1&&(e.preventDefault?e.preventDefault():e.returnValue=!1,e.stopPropagation&&e.stopPropagation(),e.cancelBubble&&(e.cancelBubble=!0)))}}function ph(e,t){const n=lt["*"];let r=e.keyCode||e.which||e.charCode;if(e.key&&e.key.toLowerCase()==="capslock"||!Lr.filter.call(this,e))return;if((r===93||r===224)&&(r=91),at.indexOf(r)===-1&&r!==229&&at.push(r),["metaKey","ctrlKey","altKey","shiftKey"].forEach(a=>{const l=ys[a];e[a]&&at.indexOf(l)===-1?at.push(l):!e[a]&&at.indexOf(l)>-1?at.splice(at.indexOf(l),1):a==="metaKey"&&e[a]&&(at=at.filter(u=>u in ys||u===r))}),r in Et){Et[r]=!0;for(const a in Cn)if(Object.prototype.hasOwnProperty.call(Cn,a)){const l=ys[Cn[a]];Lr[a]=e[l]}if(!n)return}for(const a in Et)Object.prototype.hasOwnProperty.call(Et,a)&&(Et[a]=e[ys[a]]);e.getModifierState&&!(e.altKey&&!e.ctrlKey)&&e.getModifierState("AltGraph")&&(at.indexOf(17)===-1&&at.push(17),at.indexOf(18)===-1&&at.push(18),Et[17]=!0,Et[18]=!0);const o=ri();if(n)for(let a=0;a1&&(o=Cb(Cn,e)),e=e[e.length-1],e=e==="*"?"*":is(e),e in lt||(lt[e]=[]),lt[e].push({keyup:l,keydown:u,scope:s,mods:o,shortcut:r[a],method:n,key:r[a],splitKey:c,element:i});if(typeof i<"u"&&window){if(!er.has(i)){const v=function(){let h=arguments.length>0&&arguments[0]!==void 0?arguments[0]:window.event;return ph(h,i)},p=function(){let h=arguments.length>0&&arguments[0]!==void 0?arguments[0]:window.event;ph(h,i),sN(h)};er.set(i,{keydownListener:v,keyupListenr:p,capture:f}),Zl(i,"keydown",v,f),Zl(i,"keyup",p,f)}if(!ks){const v=()=>{at=[]};ks={listener:v,capture:f},Zl(window,"focus",v,f)}}}function iN(e){let t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"all";Object.keys(lt).forEach(n=>{lt[n].filter(o=>o.scope===t&&o.shortcut===e).forEach(o=>{o&&o.method&&o.method()})})}function Zc(e){const t=Object.values(lt).flat();if(t.findIndex(r=>{let{element:o}=r;return o===e})<0){const{keydownListener:r,keyupListenr:o,capture:s}=er.get(e)||{};r&&o&&(ms(e,"keyup",o,s),ms(e,"keydown",r,s),er.delete(e))}if((t.length<=0||er.size<=0)&&(Object.keys(er).forEach(o=>{const{keydownListener:s,keyupListenr:i,capture:a}=er.get(o)||{};s&&i&&(ms(o,"keyup",i,a),ms(o,"keydown",s,a),er.delete(o))}),er.clear(),Object.keys(lt).forEach(o=>delete lt[o]),ks)){const{listener:o,capture:s}=ks;ms(window,"focus",o,s),ks=null}}const Ql={getPressedKeyString:eN,setScope:Ab,getScope:ri,deleteScope:oN,getPressedKeyCodes:Q8,getAllKeyCodes:tN,isPressed:rN,filter:nN,trigger:iN,unbind:Rb,keyMap:ni,modifier:Cn,modifierMap:ys};for(const e in Ql)Object.prototype.hasOwnProperty.call(Ql,e)&&(Lr[e]=Ql[e]);if(typeof window<"u"){const e=window.hotkeys;Lr.noConflict=t=>(t&&window.hotkeys===Lr&&(window.hotkeys=e),Lr),window.hotkeys=Lr}const Xo=y8?window:void 0;function xb(e){var t;const n=ln(e);return(t=n==null?void 0:n.$el)!=null?t:n}function hh(...e){let t,n,r,o;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,o]=e,t=Xo):[t,n,r,o]=e,!t)return Jo;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],i=()=>{s.forEach(c=>c()),s.length=0},a=(c,f,d,v)=>(c.addEventListener(f,d,v),()=>c.removeEventListener(f,d,v)),l=he(()=>[xb(t),ln(o)],([c,f])=>{if(i(),!c)return;const d=S8(f)?{...f}:f;s.push(...n.flatMap(v=>r.map(p=>a(c,v,p,d))))},{immediate:!0,flush:"post"}),u=()=>{l(),i()};return mb(u),u}function aN(){const e=V(!1),t=Ge();return t&&Ke(()=>{e.value=!0},t),e}function lN(e){const t=aN();return C(()=>(t.value,!!e()))}function uN(e,t={}){const{window:n=Xo}=t,r=lN(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const s=V(!1),i=u=>{s.value=u.matches},a=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",i):o.removeListener(i))},l=Ha(()=>{r.value&&(a(),o=n.matchMedia(ln(e)),"addEventListener"in o?o.addEventListener("change",i):o.addListener(i),s.value=o.matches)});return mb(()=>{l(),a(),o=void 0}),s}const zi=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},Hi="__vueuse_ssr_handlers__",cN=fN();function fN(){return Hi in zi||(zi[Hi]=zi[Hi]||{}),zi[Hi]}function Ib(e,t){return cN[e]||t}function Pb(e){return uN("(prefers-color-scheme: dark)",e)}function dN(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const pN={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},vh="vueuse-storage";function hN(e,t,n,r={}){var o;const{flush:s="pre",deep:i=!0,listenToStorageChanges:a=!0,writeDefaults:l=!0,mergeDefaults:u=!1,shallow:c,window:f=Xo,eventFilter:d,onError:v=x=>{},initOnMounted:p}=r,h=(c?ir:V)(typeof t=="function"?t():t);if(!n)try{n=Ib("getDefaultStorage",()=>{var x;return(x=Xo)==null?void 0:x.localStorage})()}catch(x){v(x)}if(!n)return h;const b=ln(t),m=dN(b),S=(o=r.serializer)!=null?o:pN[m],{pause:_,resume:w}=R8(h,()=>A(h.value),{flush:s,deep:i,eventFilter:d});f&&a&&bb(()=>{n instanceof Storage?hh(f,"storage",N):hh(f,vh,I),p&&N()}),p||N();function y(x,k){if(f){const $={key:e,oldValue:x,newValue:k,storageArea:n};f.dispatchEvent(n instanceof Storage?new StorageEvent("storage",$):new CustomEvent(vh,{detail:$}))}}function A(x){try{const k=n.getItem(e);if(x==null)y(k,null),n.removeItem(e);else{const $=S.write(x);k!==$&&(n.setItem(e,$),y(k,$))}}catch(k){v(k)}}function R(x){const k=x?x.newValue:n.getItem(e);if(k==null)return l&&b!=null&&n.setItem(e,S.write(b)),b;if(!x&&u){const $=S.read(k);return typeof u=="function"?u($,b):m==="object"&&!Array.isArray($)?{...b,...$}:$}else return typeof k!="string"?k:S.read(k)}function N(x){if(!(x&&x.storageArea!==n)){if(x&&x.key==null){h.value=b;return}if(!(x&&x.key!==e)){_();try{(x==null?void 0:x.newValue)!==S.write(h.value)&&(h.value=R(x))}catch(k){v(k)}finally{x?Ie(w):w()}}}}function I(x){N(x.detail)}return h}const vN="*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}";function mN(e={}){const{selector:t="html",attribute:n="class",initialValue:r="auto",window:o=Xo,storage:s,storageKey:i="vueuse-color-scheme",listenToStorageChanges:a=!0,storageRef:l,emitAuto:u,disableTransition:c=!0}=e,f={auto:"",light:"light",dark:"dark",...e.modes||{}},d=Pb({window:o}),v=C(()=>d.value?"dark":"light"),p=l||(i==null?O8(r):hN(i,r,s,{window:o,listenToStorageChanges:a})),h=C(()=>p.value==="auto"?v.value:p.value),b=Ib("updateHTMLAttrs",(w,y,A)=>{const R=typeof w=="string"?o==null?void 0:o.document.querySelector(w):xb(w);if(!R)return;const N=new Set,I=new Set;let x=null;if(y==="class"){const $=A.split(/\s/g);Object.values(f).flatMap(F=>(F||"").split(/\s/g)).filter(Boolean).forEach(F=>{$.includes(F)?N.add(F):I.add(F)})}else x={key:y,value:A};if(N.size===0&&I.size===0&&x===null)return;let k;c&&(k=o.document.createElement("style"),k.appendChild(document.createTextNode(vN)),o.document.head.appendChild(k));for(const $ of N)R.classList.add($);for(const $ of I)R.classList.remove($);x&&R.setAttribute(x.key,x.value),c&&(o.getComputedStyle(k).opacity,document.head.removeChild(k))});function m(w){var y;b(t,n,(y=f[w])!=null?y:w)}function S(w){e.onChanged?e.onChanged(w,m):m(w)}he(h,S,{flush:"post",immediate:!0}),bb(()=>S(h.value));const _=C({get(){return u?p.value:h.value},set(w){p.value=w}});try{return Object.assign(_,{store:p,system:v,state:h})}catch{return _}}function yL(e={}){const{valueDark:t="dark",valueLight:n="",window:r=Xo}=e,o=mN({...e,onChanged:(a,l)=>{var u;e.onChanged?(u=e.onChanged)==null||u.call(e,a==="dark",l,a):l(a)},modes:{dark:t,light:n}}),s=C(()=>o.system?o.system.value:Pb({window:r}).value?"dark":"light");return C({get(){return o.value==="dark"},set(a){const l=a?"dark":"light";s.value===l?o.value="auto":o.value=l}})}const wL='';export{VN as $,Ex as A,ON as B,C,bg as D,kN as E,nt as F,xN as G,Ue as H,AN as I,TN as J,CN as K,LN as L,V as M,he as N,Vt as O,ae as P,Ha as Q,vf as R,zN as S,jN as T,Ke as U,bL as V,MN as W,_N as X,Lr as Y,IN as Z,PN as _,SN as a,$N as a0,FN as a1,BN as a2,yL as a3,bN as a4,Ux as a5,gL as a6,EN as a7,qN as a8,kr as a9,ot as aa,Ie as ab,Rg as ac,ct as ad,fw as ae,DN as af,dL as ag,mo as ah,pL as ai,yN as aj,wL as ak,UN as al,wN as b,fe as c,Z as d,ee as e,re as f,Un as g,le as h,HN as i,mt as j,hL as k,RN as l,mL as m,KN as n,L as o,Ds as p,vL as q,Yt as r,ir as s,Me as t,g as u,gw as v,ce as w,NN as x,U as y,He as z}; diff --git a/app/src/main/assets/web/vue/favicon.ico b/app/src/main/assets/web/vue/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..c3d5e6bc00d62252bb516ab1d4ca3d9aa40986aa GIT binary patch literal 4286 zcmb_gdq|X76u*59tx&kK&}1YkB+DZGQ9@uOD$&Et%I9b%Jt(AyuOfPynpyOqd>iF6 zci2p)&0a#ns+$&>>!Vy;N===<869WTsRr_oVb7j(^&3aCb;@oW?{~j@?)m-hd3{5S z&B1@Ju8jV&G6%-wjIm!(@D=k#q4%>Lj*gDZ#l=N%aB#4b%jNd-=g7vH8=ky{@vbgZh!d_ zT3cG6si_IHS}hn11}-|C&fMMIEk1ejM1?#RPEJm8vhgh(TwPu5wr<_(QB(6rWmjjX z>FwKY=zQ}A{^{s|_E)cUVm|O><*o({SaD8$6X2#+5> z_CstMFdO`Lq8@Szd*RP#V;t(A_d|9;5A^owd3|8LnlQ%x`}Xa#_+b6|^>(5tDnEVt z1hdB17$C1y4H<=faIabm={V=&x>wEb&p9+VH<$VN`0$$S>FHrrRaI^uK73Gr4}4K; zfC7~UvXr7l@+&mZH|Bqi-`CeC=H}-9{PgKlwrkfehIJl{*yhpE(OKVL({F%Z9}U2b zyAx81Yc={ss)fP9K{Li%5gi>ZY~H+Cz?@H|yMHZyZA}gK!4`feDc7|F2Dn{ofRgG7 zDZ}+U^oyiJJpp5o78MmGY}&L*Fd9dSz6PIq|Gmmeh>wedg9i@+){mtRYjs0#_xT8v zHB1Pt#evB++0oHax_tR^0egZBbFXq_WSINjWctYSe`v_SdtX;qC$zP;LS0=gl$VzS z_ELzAiGi@NFu?Bvm(b8qh(j*;{e!#r9zxN>Rw%7`3uVuAplUM0!8XxtoNan3%|OfqX%EL3uLO z9^#lbY}nvUn9`s)pq>mxi83h<2p@YjP`*=*l20#Py7U?TjT`w~hkQf&cOZ z{taUfu$JJ;qVyB98AyvT8>P6`SN zEOy8y#ogcE-*GCo!~=7g@?b5>u*92uN;RIEn#${)VnDvVdiAQsHubHHj0}z+8ygE1 z6&3vc*xxNWXwQguUrQJg-gGneCyGNPaUjl=+vHQSMRjoCzyaQGtZh^LDMzTc?cTi` z($mv{YLR%2`_hPWB*t&r-+tCS$T!3RYnuF^Mln-;<7~4?J%Y}T+gOWSK8v|E?$l@W zqky=S50sOi)djwri4UDok`1yo)xIrj@&)+}ZFr$jf1n;UsXnIgpgqzlqBC^%OSxtX zKIzbT?A6pC$RAEqvdg`uvxkfySr{K;2vb#^7JZlFf|yj56k8WbDUY#(p5FV{EP@DfnLsQw8_tNc%MR Uj-MkNWQ%N)ZNlJKW*?yOA4v{VRR910 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/web/vue/index.html b/app/src/main/assets/web/vue/index.html new file mode 100755 index 000000000..384519fe0 --- /dev/null +++ b/app/src/main/assets/web/vue/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + +
    + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100755 index 0000000000000000000000000000000000000000..7e2060a9c6b8c27b3e6ff45c77b3c28b3fd454cf GIT binary patch literal 204579 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 { + ChineseUtils.fixT2sDict() + ChineseUtils.preLoad(true, TransType.TRADITIONAL_TO_SIMPLE) + } + + 2 -> ChineseUtils.preLoad(true, TransType.SIMPLE_TO_TRADITIONAL) + } + //调整排序序号 + SourceHelp.adjustSortNumber() + //同步阅读记录 + if (AppConfig.syncBookProgress) { + AppWebDav.downloadAllBookProgress() + } + } + } + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(AppContextWrapper.wrap(base)) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val diff = newConfig.diff(oldConfig) + if ((diff and ActivityInfo.CONFIG_UI_MODE) != 0) { + applyDayNight(this) + } + oldConfig = Configuration(newConfig) + } + + /** + * 尝试在安装了GMS的设备上(GMS或者MicroG)使用GMS内置的Conscrypt + * 作为首选JCE提供程序,而使Okhttp在低版本Android上 + * 能够启用TLSv1.3 + * https://f-droid.org/zh_Hans/2020/05/29/android-updates-and-tls-connections.html + * https://developer.android.google.cn/reference/javax/net/ssl/SSLSocket + * + * @param context + * @return + */ + private fun installGmsTlsProvider(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return + } + try { + val gmsPackageName = "com.google.android.gms" + val appInfo = packageManager.getApplicationInfo(gmsPackageName, 0) + if ((appInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 0) { + return + } + val gms = context.createPackageContext( + gmsPackageName, + CONTEXT_INCLUDE_CODE or CONTEXT_IGNORE_SECURITY + ) + gms.classLoader + .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") + .getMethod("insertProvider", Context::class.java) + .invoke(null, gms) + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + } + + /** + * 创建通知ID + */ + private fun createNotificationChannels() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val downloadChannel = NotificationChannel( + channelIdDownload, + getString(R.string.action_download), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + enableLights(false) + enableVibration(false) + setSound(null, null) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } + + val readAloudChannel = NotificationChannel( + channelIdReadAloud, + getString(R.string.read_aloud), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + enableLights(false) + enableVibration(false) + setSound(null, null) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } + + val webChannel = NotificationChannel( + channelIdWeb, + getString(R.string.web_service), + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + enableLights(false) + enableVibration(false) + setSound(null, null) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } + + //向notification manager 提交channel + notificationManager.createNotificationChannels( + listOf( + downloadChannel, + readAloudChannel, + webChannel + ) + ) + } + + private fun initRhino() { + RhinoScriptEngine + RhinoWrapFactory.register(BookSource::class.java, NativeBaseSource.factory) + RhinoWrapFactory.register(RssSource::class.java, NativeBaseSource.factory) + RhinoWrapFactory.register(HttpTTS::class.java, NativeBaseSource.factory) + RhinoWrapFactory.register(ExploreRule::class.java, ReadOnlyJavaObject.factory) + RhinoWrapFactory.register(SearchRule::class.java, ReadOnlyJavaObject.factory) + RhinoWrapFactory.register(BookInfoRule::class.java, ReadOnlyJavaObject.factory) + RhinoWrapFactory.register(ContentRule::class.java, ReadOnlyJavaObject.factory) + RhinoWrapFactory.register(BookChapter::class.java, ReadOnlyJavaObject.factory) + RhinoWrapFactory.register(Book.ReadConfig::class.java, ReadOnlyJavaObject.factory) + } + + class EventLogger : DefaultLogger() { + + override fun log(level: Level, msg: String) { + super.log(level, msg) + LogUtils.d(TAG, msg) + } + + override fun log(level: Level, msg: String, th: Throwable?) { + super.log(level, msg, th) + LogUtils.d(TAG, "$msg\n${th?.stackTraceToString()}") + } + + companion object { + private const val TAG = "[LiveEventBus]" + } + } + + companion object { + init { + if (BuildConfig.DEBUG) { + System.setProperty("kotlinx.coroutines.debug", "on") + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/README.md b/app/src/main/java/io/legado/app/README.md new file mode 100755 index 000000000..bc8899e58 --- /dev/null +++ b/app/src/main/java/io/legado/app/README.md @@ -0,0 +1,15 @@ +# 文件结构介绍 + +* api 提供的接口 +* base 基类 +* constant 常量 +* data 数据 +* exception 错误类型 +* help 帮助 +* lib 库 +* model 解析 +* receiver 广播侦听 +* service 服务 +* ui 界面 +* utils 辅助类 +* web web服务 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/api/ReaderProvider.kt b/app/src/main/java/io/legado/app/api/ReaderProvider.kt new file mode 100755 index 000000000..37e88240e --- /dev/null +++ b/app/src/main/java/io/legado/app/api/ReaderProvider.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 w568w + */ +package io.legado.app.api + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.UriMatcher +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import com.google.gson.Gson +import io.legado.app.api.controller.BookController +import io.legado.app.api.controller.BookSourceController +import io.legado.app.api.controller.RssSourceController +import kotlinx.coroutines.runBlocking + +/** + * Export book data to other app. + */ +class ReaderProvider : ContentProvider() { + private enum class RequestCode { + SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources, + SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources, + SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover, + SaveBookProgress + } + + private val postBodyKey = "json" + private val sMatcher by lazy { + UriMatcher(UriMatcher.NO_MATCH).apply { + "${context?.applicationInfo?.packageName}.readerProvider".also { authority -> + addURI(authority, "bookSource/insert", RequestCode.SaveBookSource.ordinal) + addURI(authority, "bookSources/insert", RequestCode.SaveBookSources.ordinal) + addURI(authority, "bookSources/delete", RequestCode.DeleteBookSources.ordinal) + addURI(authority, "bookSource/query", RequestCode.GetBookSource.ordinal) + addURI(authority, "bookSources/query", RequestCode.GetBookSources.ordinal) + addURI(authority, "rssSource/insert", RequestCode.SaveBookSource.ordinal) + addURI(authority, "rssSources/insert", RequestCode.SaveBookSources.ordinal) + addURI(authority, "rssSources/delete", RequestCode.DeleteBookSources.ordinal) + addURI(authority, "rssSource/query", RequestCode.GetBookSource.ordinal) + addURI(authority, "rssSources/query", RequestCode.GetBookSources.ordinal) + addURI(authority, "book/insert", RequestCode.SaveBook.ordinal) + addURI(authority, "books/query", RequestCode.GetBookshelf.ordinal) + addURI(authority, "book/refreshToc/query", RequestCode.RefreshToc.ordinal) + addURI(authority, "book/chapter/query", RequestCode.GetChapterList.ordinal) + addURI(authority, "book/content/query", RequestCode.GetBookContent.ordinal) + addURI(authority, "book/cover/query", RequestCode.GetBookCover.ordinal) + } + } + } + + override fun onCreate(): Boolean { + context?.let { context -> + ShortCuts.buildShortCuts(context) + } + return false + } + + override fun delete( + uri: Uri, + selection: String?, + selectionArgs: Array? + ): Int { + if (sMatcher.match(uri) < 0) return -1 + when (RequestCode.entries[sMatcher.match(uri)]) { + RequestCode.DeleteBookSources -> BookSourceController.deleteSources(selection) + RequestCode.DeleteRssSources -> BookSourceController.deleteSources(selection) + else -> throw IllegalStateException( + "Unexpected value: " + RequestCode.entries[sMatcher.match(uri)].name + ) + } + return 0 + } + + override fun getType(uri: Uri) = throw UnsupportedOperationException("Not yet implemented") + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + if (sMatcher.match(uri) < 0) return null + runBlocking { + when (RequestCode.entries[sMatcher.match(uri)]) { + RequestCode.SaveBookSource -> values?.let { + BookSourceController.saveSource(values.getAsString(postBodyKey)) + } + + RequestCode.SaveBookSources -> values?.let { + BookSourceController.saveSources(values.getAsString(postBodyKey)) + } + + RequestCode.SaveRssSource -> values?.let { + RssSourceController.saveSource(values.getAsString(postBodyKey)) + } + + RequestCode.SaveRssSources -> values?.let { + RssSourceController.saveSources(values.getAsString(postBodyKey)) + } + + RequestCode.SaveBook -> values?.let { + BookController.saveBook(values.getAsString(postBodyKey)) + } + + RequestCode.SaveBookProgress -> values?.let { + BookController.saveBookProgress(values.getAsString(postBodyKey)) + } + + else -> throw IllegalStateException( + "Unexpected value: " + RequestCode.entries[sMatcher.match(uri)].name + ) + } + } + return null + } + + override fun query( + uri: Uri, projection: Array?, selection: String?, + selectionArgs: Array?, sortOrder: String? + ): Cursor? { + val map: MutableMap> = HashMap() + uri.getQueryParameter("url")?.let { + map["url"] = arrayListOf(it) + } + uri.getQueryParameter("index")?.let { + map["index"] = arrayListOf(it) + } + uri.getQueryParameter("path")?.let { + map["path"] = arrayListOf(it) + } + return if (sMatcher.match(uri) < 0) null else when (RequestCode.entries[sMatcher.match(uri)]) { + RequestCode.GetBookSource -> SimpleCursor(BookSourceController.getSource(map)) + RequestCode.GetBookSources -> SimpleCursor(BookSourceController.sources) + RequestCode.GetRssSource -> SimpleCursor(RssSourceController.getSource(map)) + RequestCode.GetRssSources -> SimpleCursor(RssSourceController.sources) + RequestCode.GetBookshelf -> SimpleCursor(BookController.bookshelf) + RequestCode.GetBookContent -> SimpleCursor(BookController.getBookContent(map)) + RequestCode.RefreshToc -> SimpleCursor(BookController.refreshToc(map)) + RequestCode.GetChapterList -> SimpleCursor(BookController.getChapterList(map)) + RequestCode.GetBookCover -> SimpleCursor(BookController.getCover(map)) + else -> throw IllegalStateException( + "Unexpected value: " + RequestCode.entries[sMatcher.match(uri)].name + ) + } + } + + override fun update( + uri: Uri, values: ContentValues?, selection: String?, + selectionArgs: Array? + ) = throw UnsupportedOperationException("Not yet implemented") + + + /** + * Simple inner class to deliver json callback data. + * + * Only getString() makes sense. + */ + private class SimpleCursor(data: ReturnData?) : MatrixCursor(arrayOf("result"), 1) { + + private val mData: String = Gson().toJson(data) + + init { + addRow(arrayOf(mData)) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/api/ReturnData.kt b/app/src/main/java/io/legado/app/api/ReturnData.kt new file mode 100755 index 000000000..3e53df878 --- /dev/null +++ b/app/src/main/java/io/legado/app/api/ReturnData.kt @@ -0,0 +1,29 @@ +package io.legado.app.api + +import androidx.annotation.Keep + +@Keep +class ReturnData { + + var isSuccess: Boolean = false + private set + + var errorMsg: String = "未知错误,请联系开发者!" + private set + + var data: Any? = null + private set + + fun setErrorMsg(errorMsg: String): ReturnData { + this.isSuccess = false + this.errorMsg = errorMsg + return this + } + + fun setData(data: Any): ReturnData { + this.isSuccess = true + this.errorMsg = "" + this.data = data + return this + } +} diff --git a/app/src/main/java/io/legado/app/api/ShortCuts.kt b/app/src/main/java/io/legado/app/api/ShortCuts.kt new file mode 100755 index 000000000..6f9ff43e8 --- /dev/null +++ b/app/src/main/java/io/legado/app/api/ShortCuts.kt @@ -0,0 +1,63 @@ +package io.legado.app.api + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import io.legado.app.R +import io.legado.app.receiver.SharedReceiverActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.main.MainActivity + +object ShortCuts { + + private inline fun buildIntent(context: Context): Intent { + val intent = Intent(context, T::class.java) + intent.action = Intent.ACTION_VIEW + return intent + } + + private fun buildBookShelfShortCutInfo(context: Context): ShortcutInfoCompat { + val bookShelfIntent = buildIntent(context) + return ShortcutInfoCompat.Builder(context, "bookshelf") + .setShortLabel(context.getString(R.string.bookshelf)) + .setLongLabel(context.getString(R.string.bookshelf)) + .setIcon(IconCompat.createWithResource(context, R.drawable.icon_read_book)) + .setIntent(bookShelfIntent) + .build() + } + + private fun buildReadBookShortCutInfo(context: Context): ShortcutInfoCompat { + val bookShelfIntent = buildIntent(context) + val readBookIntent = buildIntent(context) + return ShortcutInfoCompat.Builder(context, "lastRead") + .setShortLabel(context.getString(R.string.last_read)) + .setLongLabel(context.getString(R.string.last_read)) + .setIcon(IconCompat.createWithResource(context, R.drawable.icon_read_book)) + .setIntents(arrayOf(bookShelfIntent, readBookIntent)) + .build() + } + + private fun buildReadAloudShortCutInfo(context: Context): ShortcutInfoCompat { + val readAloudIntent = buildIntent(context) + readAloudIntent.putExtra("action", "readAloud") + return ShortcutInfoCompat.Builder(context, "readAloud") + .setShortLabel(context.getString(R.string.read_aloud)) + .setLongLabel(context.getString(R.string.read_aloud)) + .setIcon(IconCompat.createWithResource(context, R.drawable.icon_read_book)) + .setIntent(readAloudIntent) + .build() + } + + fun buildShortCuts(context: Context) { + ShortcutManagerCompat.setDynamicShortcuts( + context, listOf( + buildReadBookShortCutInfo(context), + buildReadAloudShortCutInfo(context), + buildBookShelfShortCutInfo(context) + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/api/controller/BookController.kt b/app/src/main/java/io/legado/app/api/controller/BookController.kt new file mode 100755 index 000000000..d6648fe09 --- /dev/null +++ b/app/src/main/java/io/legado/app/api/controller/BookController.kt @@ -0,0 +1,325 @@ +package io.legado.app.api.controller + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import androidx.core.graphics.drawable.toBitmap +import com.bumptech.glide.Glide +import io.legado.app.api.ReturnData +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookProgress +import io.legado.app.data.entities.BookSource +import io.legado.app.help.AppWebDav +import io.legado.app.help.CacheManager +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.help.glide.ImageLoader +import io.legado.app.model.BookCover +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadBook +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.GSON +import io.legado.app.utils.cnCompare +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.stackTraceStr +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import splitties.init.appCtx +import java.io.File +import java.util.WeakHashMap +import java.util.concurrent.TimeUnit + +object BookController { + + private lateinit var book: Book + private var bookSource: BookSource? = null + private var bookUrl: String = "" + private val defaultCoverCache by lazy { WeakHashMap() } + + /** + * 书架所有书籍 + */ + val bookshelf: ReturnData + get() { + val books = appDb.bookDao.all + val returnData = ReturnData() + return if (books.isEmpty()) { + returnData.setErrorMsg("还没有添加小说") + } else { + val data = when (AppConfig.bookshelfSort) { + 1 -> books.sortedByDescending { it.latestChapterTime } + 2 -> books.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + + 3 -> books.sortedBy { it.order } + else -> books.sortedByDescending { it.durChapterTime } + } + returnData.setData(data) + } + } + + /** + * 获取封面 + */ + fun getCover(parameters: Map>): ReturnData { + val returnData = ReturnData() + val coverPath = parameters["path"]?.firstOrNull() + val ftBitmap = ImageLoader.loadBitmap(appCtx, coverPath) + .override(84, 112) + .centerCrop() + .submit() + return try { + returnData.setData(ftBitmap.get(3, TimeUnit.SECONDS)) + } catch (e: Exception) { + try { + val defaultBitmap = defaultCoverCache.getOrPut(BookCover.defaultDrawable) { + Glide.with(appCtx) + .asBitmap() + .load(BookCover.defaultDrawable.toBitmap()) + .override(84, 112) + .centerCrop() + .submit() + .get() + } + returnData.setData(defaultBitmap) + } catch (e: Exception) { + returnData.setErrorMsg(e.localizedMessage ?: "getCover error") + } + } + } + + /** + * 获取正文图片 + */ + fun getImg(parameters: Map>): ReturnData { + val returnData = ReturnData() + val bookUrl = parameters["url"]?.firstOrNull() + ?: return returnData.setErrorMsg("bookUrl为空") + val src = parameters["path"]?.firstOrNull() + ?: return returnData.setErrorMsg("图片链接为空") + val width = parameters["width"]?.firstOrNull()?.toInt() ?: 640 + if (this.bookUrl != bookUrl) { + this.book = appDb.bookDao.getBook(bookUrl) + ?: return returnData.setErrorMsg("bookUrl不对") + this.bookSource = appDb.bookSourceDao.getBookSource(book.origin) + } + this.bookUrl = bookUrl + val bitmap = runBlocking { + ImageProvider.cacheImage(book, src, bookSource) + ImageProvider.getImage(book, src, width) + } + return returnData.setData(bitmap) + } + + /** + * 更新目录 + */ + fun refreshToc(parameters: Map>): ReturnData { + val returnData = ReturnData() + try { + val bookUrl = parameters["url"]?.firstOrNull() + if (bookUrl.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") + } + val book = appDb.bookDao.getBook(bookUrl) + ?: return returnData.setErrorMsg("未在数据库找到对应书籍,请先添加") + if (book.isLocal) { + val toc = LocalBook.getChapterList(book) + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + appDb.bookDao.update(book) + return returnData.setData(toc) + } else { + val bookSource = appDb.bookSourceDao.getBookSource(book.origin) + ?: return returnData.setErrorMsg("未找到对应书源,请换源") + val toc = runBlocking { + if (book.tocUrl.isBlank()) { + WebBook.getBookInfoAwait(bookSource, book) + } + WebBook.getChapterListAwait(bookSource, book).getOrThrow() + } + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + appDb.bookDao.update(book) + return returnData.setData(toc) + } + } catch (e: Exception) { + return returnData.setErrorMsg(e.localizedMessage ?: "refresh toc error") + } + } + + /** + * 获取目录 + */ + fun getChapterList(parameters: Map>): ReturnData { + val bookUrl = parameters["url"]?.firstOrNull() + val returnData = ReturnData() + if (bookUrl.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") + } + val chapterList = appDb.bookChapterDao.getChapterList(bookUrl) + if (chapterList.isEmpty()) { + return refreshToc(parameters) + } + return returnData.setData(chapterList) + } + + /** + * 获取正文 + */ + fun getBookContent(parameters: Map>): ReturnData { + val bookUrl = parameters["url"]?.firstOrNull() + val index = parameters["index"]?.firstOrNull()?.toInt() + val returnData = ReturnData() + if (bookUrl.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书籍地址") + } + if (index == null) { + return returnData.setErrorMsg("参数index不能为空, 请指定目录序号") + } + val book = appDb.bookDao.getBook(bookUrl) + val chapter = runBlocking { + var chapter = appDb.bookChapterDao.getChapter(bookUrl, index) + var wait = 0 + while (chapter == null && wait < 30) { + delay(1000) + chapter = appDb.bookChapterDao.getChapter(bookUrl, index) + wait++ + } + chapter + } + if (book == null || chapter == null) { + return returnData.setErrorMsg("未找到") + } + var content: String? = BookHelp.getContent(book, chapter) + if (content != null) { + val contentProcessor = ContentProcessor.get(book.name, book.origin) + content = runBlocking { + contentProcessor.getContent(book, chapter, content, includeTitle = false) + .toString() + } + return returnData.setData(content) + } + val bookSource = appDb.bookSourceDao.getBookSource(book.origin) + ?: return returnData.setErrorMsg("未找到书源") + try { + content = runBlocking { + WebBook.getContentAwait(bookSource, book, chapter).let { + val contentProcessor = ContentProcessor.get(book.name, book.origin) + contentProcessor.getContent(book, chapter, it, includeTitle = false) + .toString() + } + } + returnData.setData(content) + } catch (e: Exception) { + returnData.setErrorMsg(e.stackTraceStr) + } + return returnData + } + + /** + * 保存书籍 + */ + suspend fun saveBook(postData: String?): ReturnData { + val returnData = ReturnData() + GSON.fromJsonObject(postData).getOrNull()?.let { book -> + AppWebDav.uploadBookProgress(book) + book.save() + return returnData.setData("") + } + return returnData.setErrorMsg("格式不对") + } + + /** + * 删除书籍 + */ + fun deleteBook(postData: String?): ReturnData { + val returnData = ReturnData() + GSON.fromJsonObject(postData).getOrNull()?.let { book -> + book.delete() + return returnData.setData("") + } + return returnData.setErrorMsg("格式不对") + } + + /** + * 保存进度 + */ + suspend fun saveBookProgress(postData: String?): ReturnData { + val returnData = ReturnData() + GSON.fromJsonObject(postData) + .onFailure { it.printOnDebug() } + .getOrNull()?.let { bookProgress -> + appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book -> + book.durChapterIndex = bookProgress.durChapterIndex + book.durChapterPos = bookProgress.durChapterPos + book.durChapterTitle = bookProgress.durChapterTitle + book.durChapterTime = bookProgress.durChapterTime + AppWebDav.uploadBookProgress(bookProgress) { + book.syncTime = System.currentTimeMillis() + } + appDb.bookDao.update(book) + ReadBook.book?.let { + if (it.name == bookProgress.name && + it.author == bookProgress.author + ) { + ReadBook.webBookProgress = bookProgress + } + } + return returnData.setData("") + } + } + return returnData.setErrorMsg("格式不对") + } + + /** + * 添加本地书籍 + */ + fun addLocalBook( + parameters: Map>, + files: Map + ): ReturnData { + val returnData = ReturnData() + val fileName = parameters["fileName"]?.firstOrNull() + ?: return returnData.setErrorMsg("fileName 不能为空") + val fileData = files["fileData"] + ?: return returnData.setErrorMsg("fileData 不能为空") + kotlin.runCatching { + val uri = LocalBook.saveBookFile(File(fileData).inputStream(), fileName) + LocalBook.importFile(uri) + }.onFailure { + return when (it) { + is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!") + else -> returnData.setErrorMsg("保存书籍错误\n${it.localizedMessage}") + } + } + return returnData.setData(true) + } + + /** + * 保存web阅读界面配置 + */ + fun saveWebReadConfig(postData: String?): ReturnData { + val returnData = ReturnData() + postData?.let { + CacheManager.put("webReadConfig", postData) + } ?: CacheManager.delete("webReadConfig") + return returnData.setData("") + } + + /** + * 获取web阅读界面配置 + */ + fun getWebReadConfig(): ReturnData { + val returnData = ReturnData() + val data = CacheManager.get("webReadConfig") + ?: return returnData.setErrorMsg("没有配置") + return returnData.setData(data) + } + +} diff --git a/app/src/main/java/io/legado/app/api/controller/BookSourceController.kt b/app/src/main/java/io/legado/app/api/controller/BookSourceController.kt new file mode 100755 index 000000000..f3c593a71 --- /dev/null +++ b/app/src/main/java/io/legado/app/api/controller/BookSourceController.kt @@ -0,0 +1,80 @@ +package io.legado.app.api.controller + + +import android.text.TextUtils +import io.legado.app.api.ReturnData +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject + +object BookSourceController { + + val sources: ReturnData + get() { + val bookSources = appDb.bookSourceDao.all + val returnData = ReturnData() + return if (bookSources.isEmpty()) { + returnData.setErrorMsg("设备源列表为空") + } else returnData.setData(bookSources) + } + + fun saveSource(postData: String?): ReturnData { + val returnData = ReturnData() + postData ?: return returnData.setErrorMsg("数据不能为空") + val bookSource = GSON.fromJsonObject(postData).getOrNull() + if (bookSource != null) { + if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) { + returnData.setErrorMsg("源名称和URL不能为空") + } else { + appDb.bookSourceDao.insert(bookSource) + returnData.setData("") + } + } else { + returnData.setErrorMsg("转换源失败") + } + return returnData + } + + fun saveSources(postData: String?): ReturnData { + postData ?: return ReturnData().setErrorMsg("数据为空") + val okSources = arrayListOf() + val bookSources = GSON.fromJsonArray(postData).getOrNull() + if (bookSources.isNullOrEmpty()) { + return ReturnData().setErrorMsg("转换源失败") + } + bookSources.forEach { bookSource -> + if (bookSource.bookSourceName.isNotBlank() + && bookSource.bookSourceUrl.isNotBlank() + ) { + appDb.bookSourceDao.insert(bookSource) + okSources.add(bookSource) + } + } + return ReturnData().setData(okSources) + } + + fun getSource(parameters: Map>): ReturnData { + val url = parameters["url"]?.firstOrNull() + val returnData = ReturnData() + if (url.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定源地址") + } + val bookSource = appDb.bookSourceDao.getBookSource(url) + ?: return returnData.setErrorMsg("未找到源,请检查书源地址") + return returnData.setData(bookSource) + } + + fun deleteSources(postData: String?): ReturnData { + kotlin.runCatching { + GSON.fromJsonArray(postData).getOrThrow().let { + SourceHelp.deleteBookSources(it) + } + }.onFailure { + return ReturnData().setErrorMsg(it.localizedMessage ?: "数据格式错误") + } + return ReturnData().setData("已执行"/*okSources*/) + } +} diff --git a/app/src/main/java/io/legado/app/api/controller/ReplaceRuleController.kt b/app/src/main/java/io/legado/app/api/controller/ReplaceRuleController.kt new file mode 100755 index 000000000..1f664e9f4 --- /dev/null +++ b/app/src/main/java/io/legado/app/api/controller/ReplaceRuleController.kt @@ -0,0 +1,97 @@ +package io.legado.app.api.controller + +import io.legado.app.api.ReturnData +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.replace +import io.legado.app.utils.stackTraceStr + +object ReplaceRuleController { + + val allRules: ReturnData + get() { + val rules = appDb.replaceRuleDao.all + val returnData = ReturnData() + returnData.setData(GSON.toJson(rules)) + return returnData + } + + + fun saveRule(postData: String?): ReturnData { + val returnData = ReturnData() + postData ?: return returnData.setErrorMsg("数据不能为空") + val rule = GSON.fromJsonObject(postData).getOrNull() + if (rule == null) { + returnData.setErrorMsg("格式不对") + } else { + if (rule.order == Int.MIN_VALUE) { + rule.order = appDb.replaceRuleDao.maxOrder + 1 + } + appDb.replaceRuleDao.insert(rule) + } + return returnData + } + + + fun delete(postData: String?): ReturnData { + val returnData = ReturnData() + postData ?: return returnData.setErrorMsg("数据不能为空") + val rule = GSON.fromJsonObject(postData).getOrNull() + if (rule == null) { + returnData.setErrorMsg("格式不对") + } else { + appDb.replaceRuleDao.delete(rule) + } + return returnData + } + + /** + * 传入测试数据格式 + * { + * rule: Replace, + * text: "xxx" + * } + */ + fun testRule(postData: String?): ReturnData { + val returnData = ReturnData() + postData ?: return returnData.setErrorMsg("数据不能为空") + val map = GSON.fromJsonObject>(postData).getOrNull() + if (map == null) { + returnData.setErrorMsg("格式不对") + } else { + val rule = map["rule"]?.let { + if (it is String) { + GSON.fromJsonObject(it).getOrNull() + } else { + GSON.fromJsonObject(GSON.toJson(it)).getOrNull() + } + } + if (rule == null) { + returnData.setErrorMsg("格式不对") + return returnData + } + if (rule.pattern.isEmpty()) { + returnData.setErrorMsg("替换规则不能为空") + } + val text = map["text"] as String + val content = try { + if (rule.isRegex) { + text.replace( + rule.pattern.toRegex(), + rule.replacement, + rule.getValidTimeoutMillisecond() + ) + } else { + text.replace(rule.pattern, rule.replacement) + } + } catch (e: Exception) { + e.stackTraceStr + } + returnData.setData(content) + } + return returnData + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/api/controller/RssSourceController.kt b/app/src/main/java/io/legado/app/api/controller/RssSourceController.kt new file mode 100755 index 000000000..51d5f5dad --- /dev/null +++ b/app/src/main/java/io/legado/app/api/controller/RssSourceController.kt @@ -0,0 +1,77 @@ +package io.legado.app.api.controller + + +import android.text.TextUtils +import io.legado.app.api.ReturnData +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject + +object RssSourceController { + + val sources: ReturnData + get() { + val source = appDb.rssSourceDao.all + val returnData = ReturnData() + return if (source.isEmpty()) { + returnData.setErrorMsg("源列表为空") + } else returnData.setData(source) + } + + fun saveSource(postData: String?): ReturnData { + val returnData = ReturnData() + postData ?: return returnData.setErrorMsg("数据不能为空") + GSON.fromJsonObject(postData).onFailure { + returnData.setErrorMsg("转换源失败${it.localizedMessage}") + }.onSuccess { source -> + if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) { + returnData.setErrorMsg("源名称和URL不能为空") + } else { + appDb.rssSourceDao.insert(source) + returnData.setData("") + } + } + return returnData + } + + fun saveSources(postData: String?): ReturnData { + postData ?: return ReturnData().setErrorMsg("数据不能为空") + val okSources = arrayListOf() + val source = GSON.fromJsonArray(postData).getOrNull() + if (source.isNullOrEmpty()) { + return ReturnData().setErrorMsg("转换源失败") + } + for (rssSource in source) { + if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) { + continue + } + appDb.rssSourceDao.insert(rssSource) + okSources.add(rssSource) + } + return ReturnData().setData(okSources) + } + + fun getSource(parameters: Map>): ReturnData { + val url = parameters["url"]?.firstOrNull() + val returnData = ReturnData() + if (url.isNullOrEmpty()) { + return returnData.setErrorMsg("参数url不能为空,请指定书源地址") + } + val source = appDb.rssSourceDao.getByKey(url) + ?: return returnData.setErrorMsg("未找到源,请检查源地址") + return returnData.setData(source) + } + + fun deleteSources(postData: String?): ReturnData { + postData ?: return ReturnData().setErrorMsg("没有传递数据") + GSON.fromJsonArray(postData).onFailure { + return ReturnData().setErrorMsg("格式不对") + }.onSuccess { + SourceHelp.deleteRssSources(it) + } + return ReturnData().setData("已执行"/*okSources*/) + } +} diff --git a/app/src/main/java/io/legado/app/base/AppContextWrapper.kt b/app/src/main/java/io/legado/app/base/AppContextWrapper.kt new file mode 100755 index 000000000..08eaf6bae --- /dev/null +++ b/app/src/main/java/io/legado/app/base/AppContextWrapper.kt @@ -0,0 +1,99 @@ +package io.legado.app.base + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Build +import android.os.LocaleList +import io.legado.app.constant.PreferKey +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.getPrefString +import io.legado.app.utils.sysConfiguration +import java.util.* + + +@Suppress("unused") +object AppContextWrapper { + + @SuppressLint("ObsoleteSdkInt") + fun wrap(context: Context): Context { + val resources: Resources = context.resources + val configuration: Configuration = resources.configuration + val targetLocale = getSetLocale(context) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + configuration.setLocale(targetLocale) + configuration.setLocales(LocaleList(targetLocale)) + } else { + @Suppress("DEPRECATION") + configuration.locale = targetLocale + } + configuration.fontScale = getFontScale(context) + return context.createConfigurationContext(configuration) + } + + fun getFontScale(context: Context): Float { + var fontScale = context.getPrefInt(PreferKey.fontScale) / 10f + if (fontScale !in 0.8f..1.6f) { + fontScale = sysConfiguration.fontScale + } + return fontScale + } + + /** + * 当前系统语言 + */ + @SuppressLint("ObsoleteSdkInt") + private fun getSystemLocale(): Locale { + val locale: Locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言 + locale = sysConfiguration.locales.get(0) + } else { + @Suppress("DEPRECATION") + locale = sysConfiguration.locale + } + return locale + } + + /** + * 当前App语言 + */ + @SuppressLint("ObsoleteSdkInt") + private fun getAppLocale(context: Context): Locale { + val locale: Locale + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + locale = context.resources.configuration.locales[0] + } else { + @Suppress("DEPRECATION") + locale = context.resources.configuration.locale + } + return locale + + } + + /** + * 当前设置语言 + */ + private fun getSetLocale(context: Context): Locale { + return when (context.getPrefString(PreferKey.language)) { + "zh" -> Locale.SIMPLIFIED_CHINESE + "tw" -> Locale.TRADITIONAL_CHINESE + "en" -> Locale.ENGLISH + else -> getSystemLocale() + } + } + + /** + * 判断App语言和设置语言是否相同 + */ + fun isSameWithSetting(context: Context): Boolean { + val locale = getAppLocale(context) + val language = locale.language + val country = locale.country + val pfLocale = getSetLocale(context) + val pfLanguage = pfLocale.language + val pfCountry = pfLocale.country + return language == pfLanguage && country == pfCountry + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseActivity.kt b/app/src/main/java/io/legado/app/base/BaseActivity.kt new file mode 100755 index 000000000..2ba8dfaa8 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseActivity.kt @@ -0,0 +1,216 @@ +package io.legado.app.base + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Configuration +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import android.os.Bundle +import android.util.AttributeSet +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.widget.FrameLayout +import androidx.activity.addCallback +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.Theme +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.TitleBar +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.applyBackgroundTint +import io.legado.app.utils.applyOpenTint +import io.legado.app.utils.applyTint +import io.legado.app.utils.disableAutoFill +import io.legado.app.utils.fullScreen +import io.legado.app.utils.hideSoftInput +import io.legado.app.utils.setLightStatusBar +import io.legado.app.utils.setNavigationBarColorAuto +import io.legado.app.utils.setStatusBarColorAuto +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.windowSize + + +abstract class BaseActivity( + val fullScreen: Boolean = true, + private val theme: Theme = Theme.Auto, + private val toolBarTheme: Theme = Theme.Auto, + private val transparent: Boolean = false, + private val imageBg: Boolean = true +) : AppCompatActivity() { + + protected abstract val binding: VB + + val isInMultiWindow: Boolean + @SuppressLint("ObsoleteSdkInt") + get() { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isInMultiWindowMode + } else { + false + } + } + + override fun attachBaseContext(newBase: Context) { + super.attachBaseContext(AppContextWrapper.wrap(newBase)) + } + + override fun onCreateView( + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + if (AppConst.menuViewNames.contains(name) && parent?.parent is FrameLayout) { + (parent.parent as View).setBackgroundColor(backgroundColor) + } + return super.onCreateView(parent, name, context, attrs) + } + + @SuppressLint("ObsoleteSdkInt") + override fun onCreate(savedInstanceState: Bundle?) { + window.decorView.disableAutoFill() + initTheme() + super.onCreate(savedInstanceState) + setupSystemBar() + setContentView(binding.root) + upBackgroundImage() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen) + } + onBackPressedDispatcher.addCallback(this) { + finish() + } + observeLiveBus() + onActivityCreated(savedInstanceState) + } + + @RequiresApi(Build.VERSION_CODES.O) + override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) { + super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) + findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen) + setupSystemBar() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(isInMultiWindow, fullScreen) + setupSystemBar() + } + + abstract fun onActivityCreated(savedInstanceState: Bundle?) + + final override fun onCreateOptionsMenu(menu: Menu): Boolean { + val bool = onCompatCreateOptionsMenu(menu) + menu.applyTint(this, toolBarTheme) + return bool + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.applyOpenTint(this) + return super.onMenuOpened(featureId, menu) + } + + open fun onCompatCreateOptionsMenu(menu: Menu) = super.onCreateOptionsMenu(menu) + + final override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + supportFinishAfterTransition() + return true + } + return onCompatOptionsItemSelected(item) + } + + open fun onCompatOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item) + + open fun initTheme() { + when (theme) { + Theme.Transparent -> setTheme(R.style.AppTheme_Transparent) + Theme.Dark -> { + setTheme(R.style.AppTheme_Dark) + window.decorView.applyBackgroundTint(backgroundColor) + } + + Theme.Light -> { + setTheme(R.style.AppTheme_Light) + window.decorView.applyBackgroundTint(backgroundColor) + } + + else -> { + if (ColorUtils.isColorLight(primaryColor)) { + setTheme(R.style.AppTheme_Light) + } else { + setTheme(R.style.AppTheme_Dark) + } + window.decorView.applyBackgroundTint(backgroundColor) + } + } + } + + open fun upBackgroundImage() { + if (imageBg) { + try { + ThemeConfig.getBgImage(this, windowManager.windowSize)?.let { + window.decorView.background = BitmapDrawable(resources, it) + } + } catch (e: OutOfMemoryError) { + toastOnUi("背景图片太大,内存溢出") + } catch (e: Exception) { + AppLog.put("加载背景出错\n${e.localizedMessage}", e) + } + } + } + + open fun setupSystemBar() { + if (fullScreen && !isInMultiWindow) { + fullScreen() + } + val isTransparentStatusBar = AppConfig.isTransparentStatusBar + val statusBarColor = ThemeStore.statusBarColor(this, isTransparentStatusBar) + setStatusBarColorAuto(statusBarColor, isTransparentStatusBar, fullScreen) + if (toolBarTheme == Theme.Dark) { + setLightStatusBar(false) + } else if (toolBarTheme == Theme.Light) { + setLightStatusBar(true) + } + upNavigationBarColor() + } + + open fun upNavigationBarColor() { + if (AppConfig.immNavigationBar) { + setNavigationBarColorAuto(ThemeStore.navigationBarColor(this)) + } else { + val nbColor = ColorUtils.darkenColor(ThemeStore.navigationBarColor(this)) + setNavigationBarColorAuto(nbColor) + } + } + + open fun observeLiveBus() { + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + return try { + super.dispatchTouchEvent(ev) + } catch (e: IllegalArgumentException) { + e.printStackTrace() + false + } + } + + override fun finish() { + currentFocus?.hideSoftInput() + super.finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt b/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt new file mode 100755 index 000000000..da4ea99fa --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt @@ -0,0 +1,114 @@ +package io.legado.app.base + +import android.content.DialogInterface +import android.content.DialogInterface.OnDismissListener +import android.os.Build +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.WindowManager +import androidx.annotation.LayoutRes +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.utils.dpToPx +import io.legado.app.utils.setBackgroundKeepPadding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.CoroutineContext + + +abstract class BaseDialogFragment( + @LayoutRes layoutID: Int, + private val adaptationSoftKeyboard: Boolean = false +) : DialogFragment(layoutID) { + + private var onDismissListener: OnDismissListener? = null + + fun setOnDismissListener(onDismissListener: OnDismissListener?) { + this.onDismissListener = onDismissListener + } + + override fun onStart() { + super.onStart() + if (adaptationSoftKeyboard) { + dialog?.window?.setBackgroundDrawableResource(R.color.transparent) + } else if (AppConfig.isEInkMode) { + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.windowAnimations = 0 + it.attributes = attr + it.decorView.setBackgroundKeepPadding(R.color.transparent) + } + // 修改gravity的时机一般在子类的onStart方法中, 因此需要在onStart之后执行. + lifecycle.addObserver(LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + when (dialog?.window?.attributes?.gravity) { + Gravity.TOP -> view?.setBackgroundResource(R.drawable.bg_eink_border_bottom) + Gravity.BOTTOM -> view?.setBackgroundResource(R.drawable.bg_eink_border_top) + else -> { + val padding = 2.dpToPx(); + view?.setPadding(padding, padding, padding, padding) + view?.setBackgroundResource(R.drawable.bg_eink_border_dialog) + } + } + } + }) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + //不加这个android 5.0对话框顶部会有空白 + setStyle(STYLE_NO_TITLE, 0) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (adaptationSoftKeyboard) { + view.findViewById(R.id.vw_bg)?.setOnClickListener(null) + view.setOnClickListener { dismiss() } + } else if (!AppConfig.isEInkMode) { + view.setBackgroundColor(ThemeStore.backgroundColor()) + } + onFragmentCreated(view, savedInstanceState) + observeLiveBus() + } + + abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) + + override fun show(manager: FragmentManager, tag: String?) { + kotlin.runCatching { + //在每个add事务前增加一个remove事务,防止连续的add + manager.beginTransaction().remove(this).commit() + super.show(manager, tag) + }.onFailure { + AppLog.put("显示对话框失败 tag:$tag", it) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDismissListener?.onDismiss(dialog) + } + + fun execute( + scope: CoroutineScope = lifecycleScope, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ) = Coroutine.async(scope, context) { block() } + + open fun observeLiveBus() { + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseFragment.kt b/app/src/main/java/io/legado/app/base/BaseFragment.kt new file mode 100755 index 000000000..7e54012b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseFragment.kt @@ -0,0 +1,78 @@ +package io.legado.app.base + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.annotation.LayoutRes +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import io.legado.app.R +import io.legado.app.ui.widget.TitleBar +import io.legado.app.utils.applyTint + +@Suppress("MemberVisibilityCanBePrivate") +abstract class BaseFragment(@LayoutRes layoutID: Int) : Fragment(layoutID) { + + var supportToolbar: Toolbar? = null + private set + + val menuInflater: MenuInflater + @SuppressLint("RestrictedApi") + get() = SupportMenuInflater(requireContext()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + onMultiWindowModeChanged() + observeLiveBus() + onFragmentCreated(view, savedInstanceState) + } + + abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) + + override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) { + super.onMultiWindowModeChanged(isInMultiWindowMode) + onMultiWindowModeChanged() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + onMultiWindowModeChanged() + } + + private fun onMultiWindowModeChanged() { + (activity as? BaseActivity<*>)?.let { + view?.findViewById(R.id.title_bar) + ?.onMultiWindowModeChanged(it.isInMultiWindow, it.fullScreen) + } + } + + fun setSupportToolbar(toolbar: Toolbar) { + supportToolbar = toolbar + supportToolbar?.let { + it.menu.apply { + onCompatCreateOptionsMenu(this) + applyTint(requireContext()) + } + + it.setOnMenuItemClickListener { item -> + onCompatOptionsItemSelected(item) + true + } + } + } + + open fun observeLiveBus() { + } + + open fun onCompatCreateOptionsMenu(menu: Menu) { + } + + open fun onCompatOptionsItemSelected(item: MenuItem) { + } + +} diff --git a/app/src/main/java/io/legado/app/base/BasePrefDialogFragment.kt b/app/src/main/java/io/legado/app/base/BasePrefDialogFragment.kt new file mode 100755 index 000000000..db5f560c4 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BasePrefDialogFragment.kt @@ -0,0 +1,44 @@ +package io.legado.app.base + +import android.view.Gravity +import android.view.WindowManager +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.dpToPx + + +abstract class BasePrefDialogFragment( +) : DialogFragment() { + + override fun onStart() { + super.onStart() + if (AppConfig.isEInkMode) { + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.windowAnimations = 0 + it.attributes = attr + it.setBackgroundDrawableResource(R.color.transparent) + } + + // 修改gravity的时机一般在子类的onStart方法中, 因此需要在onStart之后执行. + lifecycle.addObserver(LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + when (dialog?.window?.attributes?.gravity) { + Gravity.TOP -> view?.setBackgroundResource(R.drawable.bg_eink_border_bottom) + Gravity.BOTTOM -> view?.setBackgroundResource(R.drawable.bg_eink_border_top) + else -> { + val padding = 2.dpToPx(); + view?.setPadding(padding, padding, padding, padding) + view?.setBackgroundResource(R.drawable.bg_eink_border_dialog) + } + } + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseService.kt b/app/src/main/java/io/legado/app/base/BaseService.kt new file mode 100755 index 000000000..2ac9a00df --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseService.kt @@ -0,0 +1,107 @@ +package io.legado.app.base + +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.annotation.CallSuper +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.help.LifecycleHelp +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.utils.LogUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Semaphore +import kotlin.coroutines.CoroutineContext + +abstract class BaseService : LifecycleService() { + + private val simpleName = this::class.simpleName.toString() + private var isForeground = false + + fun execute( + scope: CoroutineScope = lifecycleScope, + context: CoroutineContext = Dispatchers.IO, + start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, + semaphore: Semaphore? = null, + block: suspend CoroutineScope.() -> T + ) = Coroutine.async(scope, context, start, executeContext, semaphore, block) + + @CallSuper + override fun onCreate() { + super.onCreate() + LifecycleHelp.onServiceCreate(this) + checkPermission() + } + + @CallSuper + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + LogUtils.d(simpleName) { + "onStartCommand $intent ${intent?.toUri(0)}" + } + if (!isForeground) { + startForegroundNotification() + isForeground = true + } + return super.onStartCommand(intent, flags, startId) + } + + @CallSuper + override fun onTaskRemoved(rootIntent: Intent?) { + LogUtils.d(simpleName, "onTaskRemoved") + super.onTaskRemoved(rootIntent) + stopSelf() + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return null + } + + @CallSuper + override fun onDestroy() { + super.onDestroy() + LifecycleHelp.onServiceDestroy(this) + } + + @CallSuper + override fun onTimeout(startId: Int, fgsType: Int) { + super.onTimeout(startId, fgsType) + LogUtils.d(simpleName, "onTimeout startId:$startId fgsType:$fgsType") + stopSelf() + } + + /** + * 开启前台服务并发送通知 + */ + open fun startForegroundNotification() { + + } + + /** + * 检测通知权限和后台权限 + */ + private fun checkPermission() { + PermissionsCompat.Builder() + .addPermissions(Permissions.POST_NOTIFICATIONS) + .rationale(R.string.notification_permission_rationale) + .onGranted { + if (lifecycleScope.isActive) { + startForegroundNotification() + } + } + .request() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PermissionsCompat.Builder() + .addPermissions(Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + .rationale(R.string.ignore_battery_permission_rationale) + .request() + } + } +} diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt new file mode 100755 index 000000000..92edd1bc0 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -0,0 +1,52 @@ +package io.legado.app.base + +import android.app.Application +import android.content.Context +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import io.legado.app.App +import io.legado.app.help.coroutine.Coroutine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Semaphore +import kotlin.coroutines.CoroutineContext + +@Suppress("unused") +open class BaseViewModel(application: Application) : AndroidViewModel(application) { + + val context: Context by lazy { this.getApplication() } + + fun execute( + scope: CoroutineScope = viewModelScope, + context: CoroutineContext = Dispatchers.IO, + start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, + semaphore: Semaphore? = null, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine.async(scope, context, start, executeContext, semaphore, block) + } + + fun executeLazy( + scope: CoroutineScope = viewModelScope, + context: CoroutineContext = Dispatchers.IO, + executeContext: CoroutineContext = Dispatchers.Main, + semaphore: Semaphore? = null, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine.async( + scope, context, CoroutineStart.LAZY, executeContext, semaphore, block + ) + } + + fun submit( + scope: CoroutineScope = viewModelScope, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> Deferred + ): Coroutine { + return Coroutine.async(scope, context) { block().await() } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/README.md b/app/src/main/java/io/legado/app/base/README.md new file mode 100755 index 000000000..ba3d001f1 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/README.md @@ -0,0 +1 @@ +# 基类 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/VMBaseActivity.kt b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt new file mode 100755 index 000000000..3eeb5c91b --- /dev/null +++ b/app/src/main/java/io/legado/app/base/VMBaseActivity.kt @@ -0,0 +1,17 @@ +package io.legado.app.base + +import androidx.lifecycle.ViewModel +import androidx.viewbinding.ViewBinding +import io.legado.app.constant.Theme + +abstract class VMBaseActivity( + fullScreen: Boolean = true, + theme: Theme = Theme.Auto, + toolBarTheme: Theme = Theme.Auto, + transparent: Boolean = false, + imageBg: Boolean = true +) : BaseActivity(fullScreen, theme, toolBarTheme, transparent, imageBg) { + + protected abstract val viewModel: VM + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/VMBaseFragment.kt b/app/src/main/java/io/legado/app/base/VMBaseFragment.kt new file mode 100755 index 000000000..cc78d19dd --- /dev/null +++ b/app/src/main/java/io/legado/app/base/VMBaseFragment.kt @@ -0,0 +1,9 @@ +package io.legado.app.base + +import androidx.lifecycle.ViewModel + +abstract class VMBaseFragment(layoutID: Int) : BaseFragment(layoutID) { + + protected abstract val viewModel: VM + +} diff --git a/app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt new file mode 100755 index 000000000..d324fbfa8 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/DiffRecyclerAdapter.kt @@ -0,0 +1,223 @@ +package io.legado.app.base.adapter + +import android.content.Context +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import splitties.views.onLongClick + +/** + * Created by Invincible on 2017/12/15. + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +abstract class DiffRecyclerAdapter(protected val context: Context) : + RecyclerView.Adapter() { + + val inflater: LayoutInflater = LayoutInflater.from(context) + + private val asyncListDiffer: AsyncListDiffer by lazy { + AsyncListDiffer(this, diffItemCallback).apply { + addListListener { _, _ -> + onCurrentListChanged() + if (keepScrollPosition) { + layoutManager?.onRestoreInstanceState(layoutState) + layoutState = null + } + } + } + } + + private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null + private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null + + private var layoutManager: RecyclerView.LayoutManager? = null + private var layoutState: Parcelable? = null + + var itemAnimation: ItemAnimation? = null + + abstract val diffItemCallback: DiffUtil.ItemCallback + + open val keepScrollPosition = false + + fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { + itemClickListener = listener + } + + fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { + itemLongClickListener = listener + } + + fun bindToRecyclerView(recyclerView: RecyclerView) { + recyclerView.adapter = this + } + + fun setItems(items: List?) { + kotlin.runCatching { + if (keepScrollPosition) { + layoutState = layoutManager?.onSaveInstanceState() + } + asyncListDiffer.submitList(items?.toMutableList()) + } + } + + fun setItem(position: Int, item: ITEM) { + kotlin.runCatching { + asyncListDiffer.currentList[position] = item + notifyItemChanged(position) + } + } + + fun updateItem(item: ITEM) { + kotlin.runCatching { + val index = asyncListDiffer.currentList.indexOf(item) + if (index >= 0) { + asyncListDiffer.currentList[index] = item + notifyItemChanged(index) + } + } + } + + fun updateItem(position: Int, payload: Any) { + kotlin.runCatching { + val size = itemCount + if (position in 0 until size) { + notifyItemChanged(position, payload) + } + } + } + + fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) { + kotlin.runCatching { + val size = itemCount + if (fromPosition in 0 until size && toPosition in 0 until size) { + notifyItemRangeChanged( + fromPosition, + toPosition - fromPosition + 1, + payloads + ) + } + } + } + + fun isEmpty() = asyncListDiffer.currentList.isEmpty() + + fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty() + + fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position) + + fun getItems(): List = asyncListDiffer.currentList + + /** + * grid 模式下使用 + */ + protected open fun getSpanSize(viewType: Int, position: Int) = 1 + + final override fun getItemCount() = getItems().size + + final override fun getItemViewType(position: Int): Int { + return 0 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder(getViewBinding(parent)) + } + + protected abstract fun getViewBinding(parent: ViewGroup): VB + + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {} + + open fun onCurrentListChanged() { + //可继承 + } + + @Suppress("UNCHECKED_CAST") + final override fun onBindViewHolder( + holder: ItemViewHolder, + position: Int, + payloads: MutableList + ) { + registerListener(holder, (holder.binding as VB)) + registerItemListener(holder) + getItem(holder.layoutPosition)?.let { + convert(holder, holder.binding, it, payloads) + } + } + + private fun registerItemListener(holder: ItemViewHolder) { + if (itemClickListener != null) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + itemClickListener?.invoke(holder, it) + } + } + } + + if (itemLongClickListener != null) { + holder.itemView.onLongClick { + getItem(holder.layoutPosition)?.let { + itemLongClickListener?.invoke(holder, it) + } + } + } + } + + override fun onViewAttachedToWindow(holder: ItemViewHolder) { + super.onViewAttachedToWindow(holder) + addAnimation(holder) + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + val manager = recyclerView.layoutManager + layoutManager = manager + if (manager is GridLayoutManager) { + manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return getSpanSize(getItemViewType(position), position) + } + } + } + } + + private fun addAnimation(holder: ItemViewHolder) { + itemAnimation?.let { + if (it.itemAnimEnabled) { + if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { + startAnimation(holder, it) + it.itemAnimStartPosition = holder.layoutPosition + } + } + } + } + + protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { + item.itemAnimation?.let { + for (anim in it.getAnimators(holder.itemView)) { + anim.setDuration(item.itemAnimDuration).start() + anim.interpolator = item.itemAnimInterpolator + } + } + } + + /** + * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, + * 使用getItem(holder.layoutPosition)来获取item + */ + abstract fun convert( + holder: ItemViewHolder, + binding: VB, + item: ITEM, + payloads: MutableList + ) + + /** + * 注册事件 + */ + abstract fun registerListener(holder: ItemViewHolder, binding: VB) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt new file mode 100755 index 000000000..2285c8242 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt @@ -0,0 +1,85 @@ +package io.legado.app.base.adapter + +import android.view.animation.Interpolator +import android.view.animation.LinearInterpolator +import io.legado.app.base.adapter.animations.* + +/** + * Created by Invincible on 2017/12/15. + */ +@Suppress("unused") +class ItemAnimation private constructor() { + + var itemAnimEnabled = false + var itemAnimFirstOnly = true + var itemAnimation: BaseAnimation? = null + var itemAnimInterpolator: Interpolator = LinearInterpolator() + var itemAnimDuration: Long = 300L + var itemAnimStartPosition: Int = -1 + + fun interpolator(interpolator: Interpolator) = apply { + itemAnimInterpolator = interpolator + } + + fun duration(duration: Long) = apply { + itemAnimDuration = duration + } + + fun startPosition(startPos: Int) = apply { + itemAnimStartPosition = startPos + } + + fun animation(animationType: Int = NONE, animation: BaseAnimation? = null) = apply { + if (animation != null) { + itemAnimation = animation + } else { + when (animationType) { + FADE_IN -> itemAnimation = AlphaInAnimation() + SCALE_IN -> itemAnimation = ScaleInAnimation() + BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation() + LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation() + RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation() + } + } + } + + fun enabled(enabled: Boolean) = apply { + itemAnimEnabled = enabled + } + + fun firstOnly(firstOnly: Boolean) = apply { + itemAnimFirstOnly = firstOnly + } + + companion object { + const val NONE: Int = 0x00000000 + + /** + * Use with [.openLoadAnimation] + */ + const val FADE_IN: Int = 0x00000001 + + /** + * Use with [.openLoadAnimation] + */ + const val SCALE_IN: Int = 0x00000002 + + /** + * Use with [.openLoadAnimation] + */ + const val BOTTOM_SLIDE_IN: Int = 0x00000003 + + /** + * Use with [.openLoadAnimation] + */ + const val LEFT_SLIDE_IN: Int = 0x00000004 + + /** + * Use with [.openLoadAnimation] + */ + const val RIGHT_SLIDE_IN: Int = 0x00000005 + + fun create() = ItemAnimation() + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt new file mode 100755 index 000000000..da57d4627 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt @@ -0,0 +1,10 @@ +package io.legado.app.base.adapter + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +/** + * Created by Invincible on 2017/11/28. + */ +@Suppress("MemberVisibilityCanBePrivate") +class ItemViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt new file mode 100755 index 000000000..98090cc27 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt @@ -0,0 +1,486 @@ +package io.legado.app.base.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.util.SparseArray +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.withTimeoutOrNullAsync +import kotlinx.coroutines.ensureActive +import splitties.views.onLongClick +import java.util.Collections + +/** + * Created by Invincible on 2017/11/24. + * + * 通用的adapter 可添加header,footer,以及不同类型item + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +abstract class RecyclerAdapter(protected val context: Context) : + RecyclerView.Adapter() { + + val inflater: LayoutInflater = LayoutInflater.from(context) + + private val headerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } + private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } + + private val items: MutableList = mutableListOf() + + private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null + private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null + + private var diffJob: Coroutine<*>? = null + + var itemAnimation: ItemAnimation? = null + + fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { + itemClickListener = listener + } + + fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { + itemLongClickListener = listener + } + + fun bindToRecyclerView(recyclerView: RecyclerView) { + recyclerView.adapter = this + } + + @Synchronized + fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = headerItems.size() + headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header) + notifyItemInserted(index) + } + } + + @Synchronized + fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = getActualItemCount() + footerItems.size() + footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer) + notifyItemInserted(index) + } + } + + @Synchronized + fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = headerItems.indexOfValue(header) + if (index >= 0) { + headerItems.remove(index) + notifyItemRemoved(index) + } + } + } + + @Synchronized + fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = footerItems.indexOfValue(footer) + if (index >= 0) { + footerItems.remove(index) + notifyItemRemoved(getActualItemCount() + index - 2) + } + } + } + + @SuppressLint("NotifyDataSetChanged") + @Synchronized + fun setItems(items: List?) { + kotlin.runCatching { + if (this.items.isNotEmpty()) { + this.items.clear() + } + if (items != null) { + this.items.addAll(items) + } + notifyDataSetChanged() + onCurrentListChanged() + } + } + + @Synchronized + fun setItems( + items: List?, + itemCallback: DiffUtil.ItemCallback, + skipDiff: Boolean = false + ) { + kotlin.runCatching { + val oldItems = this.items.toList() + val itemsSize = items?.size ?: 0 + val headerCount = getHeaderCount() + val footerCount = getFooterCount() + val callback = object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return itemCount + } + + override fun getNewListSize(): Int { + return itemsSize + headerCount + footerCount + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems.getOrNull(oldItemPosition - headerCount) + ?: return true + val newItem = items?.getOrNull(newItemPosition - headerCount) + ?: return true + return itemCallback.areItemsTheSame(oldItem, newItem) + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + val oldItem = oldItems.getOrNull(oldItemPosition - headerCount) + ?: return true + val newItem = items?.getOrNull(newItemPosition - headerCount) + ?: return true + return itemCallback.areContentsTheSame(oldItem, newItem) + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val oldItem = oldItems.getOrNull(oldItemPosition - headerCount) + ?: return null + val newItem = items?.getOrNull(newItemPosition - headerCount) + ?: return null + return itemCallback.getChangePayload(oldItem, newItem) + } + } + diffJob?.cancel() + diffJob = Coroutine.async { + val diffResult = if (skipDiff) withTimeoutOrNullAsync(500L) { + DiffUtil.calculateDiff(callback, itemsSize < 2000) + } else { + DiffUtil.calculateDiff(callback, itemsSize < 2000) + } + ensureActive() + handler.post { + if (diffResult == null) { + setItems(items) + return@post + } + if (this@RecyclerAdapter.items.isNotEmpty()) { + this@RecyclerAdapter.items.clear() + } + if (items != null) { + this@RecyclerAdapter.items.addAll(items) + } + diffResult.dispatchUpdatesTo(this@RecyclerAdapter) + onCurrentListChanged() + } + } + } + } + + @Synchronized + fun setItem(position: Int, item: ITEM) { + kotlin.runCatching { + val oldSize = getActualItemCount() + if (position in 0 until oldSize) { + this.items[position] = item + notifyItemChanged(position + getHeaderCount()) + } + onCurrentListChanged() + } + } + + @Synchronized + fun addItem(item: ITEM) { + kotlin.runCatching { + val oldSize = getActualItemCount() + if (this.items.add(item)) { + notifyItemInserted(oldSize + getHeaderCount()) + } + onCurrentListChanged() + } + } + + @Synchronized + fun addItems(position: Int, newItems: List) { + kotlin.runCatching { + if (this.items.addAll(position, newItems)) { + notifyItemRangeInserted(position + getHeaderCount(), newItems.size) + } + onCurrentListChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + @Synchronized + fun addItems(newItems: List) { + kotlin.runCatching { + val oldSize = getActualItemCount() + if (this.items.addAll(newItems)) { + if (oldSize == 0 && getHeaderCount() == 0) { + notifyDataSetChanged() + } else { + notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size) + } + } + onCurrentListChanged() + } + } + + @Synchronized + fun removeItem(position: Int) { + kotlin.runCatching { + if (this.items.removeAt(position) != null) { + notifyItemRemoved(position + getHeaderCount()) + } + onCurrentListChanged() + } + } + + @Synchronized + fun removeItem(item: ITEM) { + kotlin.runCatching { + if (this.items.remove(item)) { + notifyItemRemoved(this.items.indexOf(item) + getHeaderCount()) + } + onCurrentListChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + @Synchronized + fun removeItems(items: List) { + kotlin.runCatching { + if (this.items.removeAll(items)) { + notifyDataSetChanged() + } + onCurrentListChanged() + } + } + + @Synchronized + fun swapItem(oldPosition: Int, newPosition: Int) { + kotlin.runCatching { + val size = getActualItemCount() + if (oldPosition in 0 until size && newPosition in 0 until size) { + val srcPosition = oldPosition + getHeaderCount() + val targetPosition = newPosition + getHeaderCount() + Collections.swap(this.items, srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) + } + onCurrentListChanged() + } + } + + @Synchronized + fun updateItem(item: ITEM) { + kotlin.runCatching { + val index = this.items.indexOf(item) + if (index >= 0) { + this.items[index] = item + notifyItemChanged(index) + } + onCurrentListChanged() + } + } + + @Synchronized + fun updateItem(position: Int, payload: Any) { + kotlin.runCatching { + val size = getActualItemCount() + if (position in 0 until size) { + notifyItemChanged(position + getHeaderCount(), payload) + } + } + } + + @Synchronized + fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) { + kotlin.runCatching { + val size = getActualItemCount() + if (fromPosition in 0 until size && toPosition in 0 until size) { + notifyItemRangeChanged( + fromPosition + getHeaderCount(), + toPosition - fromPosition + 1, + payloads + ) + } + } + } + + @SuppressLint("NotifyDataSetChanged") + @Synchronized + fun clearItems() { + kotlin.runCatching { + this.items.clear() + notifyDataSetChanged() + onCurrentListChanged() + } + } + + fun isEmpty() = items.isEmpty() + + fun isNotEmpty() = items.isNotEmpty() + + /** + * 除去header和footer + */ + fun getActualItemCount() = items.size + + + fun getHeaderCount() = headerItems.size() + + + fun getFooterCount() = footerItems.size() + + fun getItem(position: Int): ITEM? = items.getOrNull(position) + + fun getItemByLayoutPosition(position: Int) = items.getOrNull(getActualPosition(position)) + + fun getItems(): List = items.toList() + + protected open fun getItemViewType(item: ITEM, position: Int) = 0 + + /** + * grid 模式下使用 + */ + protected open fun getSpanSize(viewType: Int, position: Int) = 1 + + final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount() + + final override fun getItemViewType(position: Int) = when { + isHeader(position) -> TYPE_HEADER_VIEW + position + isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() + else -> getItemByLayoutPosition(position)?.let { + getItemViewType(it, getActualPosition(position)) + } ?: 0 + } + + open fun onCurrentListChanged() { + + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when { + viewType < TYPE_HEADER_VIEW + getHeaderCount() -> { + ItemViewHolder(headerItems.get(viewType).invoke(parent)) + } + + viewType >= TYPE_FOOTER_VIEW -> { + ItemViewHolder(footerItems.get(viewType).invoke(parent)) + } + + else -> { + ItemViewHolder(getViewBinding(parent)) + } + } + + protected abstract fun getViewBinding(parent: ViewGroup): VB + + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {} + + @Suppress("UNCHECKED_CAST") + final override fun onBindViewHolder( + holder: ItemViewHolder, + position: Int, + payloads: MutableList + ) { + if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { + registerListener(holder, (holder.binding as VB)) + registerItemListener(holder) + getItemByLayoutPosition(holder.layoutPosition)?.let { item -> + convert(holder, holder.binding, item, payloads) + } + } + } + + private fun registerItemListener(holder: ItemViewHolder) { + if (itemClickListener != null) { + holder.itemView.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + itemClickListener?.invoke(holder, it) + } + } + } + + if (itemLongClickListener != null) { + holder.itemView.onLongClick { + getItemByLayoutPosition(holder.layoutPosition)?.let { + itemLongClickListener?.invoke(holder, it) + } + } + } + } + + override fun onViewAttachedToWindow(holder: ItemViewHolder) { + super.onViewAttachedToWindow(holder) + if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { + addAnimation(holder) + } + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + val manager = recyclerView.layoutManager + if (manager is GridLayoutManager) { + manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return getSpanSize(getItemViewType(position), position) + } + } + } + } + + private fun isHeader(position: Int) = position < getHeaderCount() + + private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount() + + private fun getActualPosition(position: Int) = position - getHeaderCount() + + private fun addAnimation(holder: ItemViewHolder) { + itemAnimation?.let { + if (it.itemAnimEnabled) { + if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { + startAnimation(holder, it) + it.itemAnimStartPosition = holder.layoutPosition + } + } + } + } + + protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { + item.itemAnimation?.let { + for (anim in it.getAnimators(holder.itemView)) { + anim.setDuration(item.itemAnimDuration).start() + anim.interpolator = item.itemAnimInterpolator + } + } + } + + /** + * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, + * 使用getItem(holder.layoutPosition)来获取item + */ + abstract fun convert( + holder: ItemViewHolder, + binding: VB, + item: ITEM, + payloads: MutableList + ) + + /** + * 注册事件 + */ + abstract fun registerListener(holder: ItemViewHolder, binding: VB) + + companion object { + private const val TYPE_HEADER_VIEW = Int.MIN_VALUE + const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999 + private val handler by lazy { buildMainHandler() } + } + +} + + + + diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt new file mode 100755 index 000000000..f5932a100 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt @@ -0,0 +1,18 @@ +package io.legado.app.base.adapter.animations + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.view.View + + +class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : + BaseAnimation { + + override fun getAnimators(view: View): Array = + arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)) + + companion object { + + private const val DEFAULT_ALPHA_FROM = 0f + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt new file mode 100755 index 000000000..735ceca57 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt @@ -0,0 +1,13 @@ +package io.legado.app.base.adapter.animations + +import android.animation.Animator +import android.view.View + +/** + * adapter item 动画 + */ +interface BaseAnimation { + + fun getAnimators(view: View): Array + +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt new file mode 100755 index 000000000..9566e2ad0 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt @@ -0,0 +1,21 @@ +package io.legado.app.base.adapter.animations + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.view.View + + +class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : + BaseAnimation { + + override fun getAnimators(view: View): Array { + val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f) + val scaleY = ObjectAnimator.ofFloat(view, "scaleY", mFrom, 1f) + return arrayOf(scaleX, scaleY) + } + + companion object { + + private const val DEFAULT_SCALE_FROM = .5f + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt new file mode 100755 index 000000000..941562201 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt @@ -0,0 +1,12 @@ +package io.legado.app.base.adapter.animations + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.view.View + +class SlideInBottomAnimation : BaseAnimation { + + + override fun getAnimators(view: View): Array = + arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt new file mode 100755 index 000000000..8cfae170b --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt @@ -0,0 +1,13 @@ +package io.legado.app.base.adapter.animations + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.view.View + + +class SlideInLeftAnimation : BaseAnimation { + + + override fun getAnimators(view: View): Array = + arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt new file mode 100755 index 000000000..e7f1c85e5 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt @@ -0,0 +1,13 @@ +package io.legado.app.base.adapter.animations + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.view.View + + +class SlideInRightAnimation : BaseAnimation { + + + override fun getAnimators(view: View): Array = + arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt new file mode 100755 index 000000000..81f50f16e --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/AppConst.kt @@ -0,0 +1,111 @@ +package io.legado.app.constant + +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.provider.Settings +import androidx.annotation.Keep +import cn.hutool.crypto.digest.DigestUtil +import io.legado.app.BuildConfig +import io.legado.app.help.update.AppVariant +import org.apache.commons.lang3.time.FastDateFormat +import splitties.init.appCtx + +@Suppress("ConstPropertyName") +@SuppressLint("SimpleDateFormat") +object AppConst { + + const val APP_TAG = "Legado" + + const val channelIdDownload = "channel_download" + const val channelIdReadAloud = "channel_read_aloud" + const val channelIdWeb = "channel_web" + + const val UA_NAME = "User-Agent" + + const val MAX_THREAD = 9 + + const val DEFAULT_WEBDAV_ID = -1L + + private const val OFFICIAL_SIGNATURE = + "8DACBF25EC667C9B1374DB1450C1A866C2AAA1173016E80BF6AD2F06FABDDC08" + private const val BETA_SIGNATURE = + "93A28468B0F69E8D14C8A99AB45841CEF902BBBA3761BBFEE02E67CBA801563E" + + val timeFormat: FastDateFormat by lazy { + FastDateFormat.getInstance("HH:mm") + } + + val dateFormat: FastDateFormat by lazy { + FastDateFormat.getInstance("yyyy/MM/dd HH:mm") + } + + val fileNameFormat: FastDateFormat by lazy { + FastDateFormat.getInstance("yy-MM-dd-HH-mm-ss") + } + + const val imagePathKey = "imagePath" + + val menuViewNames = arrayOf( + "com.android.internal.view.menu.ListMenuItemView", + "androidx.appcompat.view.menu.ListMenuItemView" + ) + + @SuppressLint("PrivateResource") + val sysElevation = appCtx.resources + .getDimension(com.google.android.material.R.dimen.design_appbar_elevation) + .toInt() + + val androidId: String by lazy { + Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID) ?: "null" + } + + val appInfo: AppInfo by lazy { + val appInfo = AppInfo() + @Suppress("DEPRECATION") + appCtx.packageManager.getPackageInfo(appCtx.packageName, PackageManager.GET_ACTIVITIES) + ?.let { + appInfo.versionName = it.versionName!! + appInfo.appVariant = when { + it.packageName.contains("releaseA") -> AppVariant.BETA_RELEASEA + isBeta -> AppVariant.BETA_RELEASE + isOfficial -> AppVariant.OFFICIAL + else -> AppVariant.UNKNOWN + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + appInfo.versionCode = it.longVersionCode + } else { + @Suppress("DEPRECATION") + appInfo.versionCode = it.versionCode.toLong() + } + } + appInfo + } + + @Suppress("DEPRECATION") + private val sha256Signature: String by lazy { + val packageInfo = + appCtx.packageManager.getPackageInfo(appCtx.packageName, PackageManager.GET_SIGNATURES) + DigestUtil.sha256Hex(packageInfo.signatures!![0].toByteArray()).uppercase() + } + + private val isOfficial = sha256Signature == OFFICIAL_SIGNATURE + + private val isBeta = sha256Signature == BETA_SIGNATURE || BuildConfig.DEBUG + + val charsets = + arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII") + + @Keep + data class AppInfo( + var versionCode: Long = 0L, + var versionName: String = "", + var appVariant: AppVariant = AppVariant.UNKNOWN + ) + + /** + * The authority of a FileProvider defined in a element in your app's manifest. + */ + const val authority = BuildConfig.APPLICATION_ID + ".fileProvider" + +} diff --git a/app/src/main/java/io/legado/app/constant/AppLog.kt b/app/src/main/java/io/legado/app/constant/AppLog.kt new file mode 100755 index 000000000..845e1a414 --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/AppLog.kt @@ -0,0 +1,64 @@ +package io.legado.app.constant + +import android.util.Log +import io.legado.app.BuildConfig +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.LogUtils +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +object AppLog { + + private val mLogs = arrayListOf>() + + val logs get() = mLogs.toList() + + @Synchronized + fun put(message: String?, throwable: Throwable? = null, toast: Boolean = false) { + message ?: return + if (toast) { + appCtx.toastOnUi(message) + } + if (mLogs.size > 100) { + mLogs.removeLastOrNull() + } + if (throwable == null) { + LogUtils.d("AppLog", message) + } else { + LogUtils.d("AppLog", "$message\n${throwable.stackTraceToString()}") + } + mLogs.add(0, Triple(System.currentTimeMillis(), message, throwable)) + if (BuildConfig.DEBUG) { + val stackTrace = Thread.currentThread().stackTrace + Log.e(stackTrace[3].className, message, throwable) + } + } + + @Synchronized + fun putNotSave(message: String?, throwable: Throwable? = null, toast: Boolean = false) { + message ?: return + if (toast) { + appCtx.toastOnUi(message) + } + if (mLogs.size > 100) { + mLogs.removeLastOrNull() + } + mLogs.add(0, Triple(System.currentTimeMillis(), message, throwable)) + if (BuildConfig.DEBUG) { + val stackTrace = Thread.currentThread().stackTrace + Log.e(stackTrace[3].className, message, throwable) + } + } + + @Synchronized + fun clear() { + mLogs.clear() + } + + fun putDebug(message: String?, throwable: Throwable? = null) { + if (AppConfig.recordLog) { + put(message, throwable) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/AppPattern.kt b/app/src/main/java/io/legado/app/constant/AppPattern.kt new file mode 100755 index 000000000..b38cc313c --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/AppPattern.kt @@ -0,0 +1,58 @@ +package io.legado.app.constant + +import java.util.regex.Pattern + +@Suppress("RegExpRedundantEscape", "unused") +object AppPattern { + val JS_PATTERN: Pattern = + Pattern.compile("([\\w\\W]*?)|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE) + val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") + + //匹配格式化后的图片格式 + val imgPattern: Pattern = Pattern.compile("]*src=['\"]([^'\"]*(?:['\"][^>]+\\})?)['\"][^>]*>") + + //dataURL图片类型 + val dataUriRegex = Regex("^data:.*?;base64,(.*)") + + val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著") + val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著") + val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") + val fileNameRegex2 = Regex("[\\\\/:*?\"<>|]") + val splitGroupRegex = Regex("[,;,;]") + val titleNumPattern: Pattern = Pattern.compile("(第)(.+?)(章)") + + //书源调试信息中的各种符号 + val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]") + + //本地书籍支持类型 + val bookFileRegex = Regex(".*\\.(txt|epub|umd|pdf|mobi|azw3|azw)", RegexOption.IGNORE_CASE) + //压缩文件支持类型 + val archiveFileRegex = Regex(".*\\.(zip|rar|7z)$", RegexOption.IGNORE_CASE) + + /** + * 所有标点 + */ + val bdRegex = Regex("(\\p{P})+") + + /** + * 换行 + */ + val rnRegex = Regex("[\\r\\n]") + + /** + * 不发音段落判断 + */ + val notReadAloudRegex = Regex("^(\\s|\\p{C}|\\p{P}|\\p{Z}|\\p{S})+$") + + val xmlContentTypeRegex = "(application|text)/\\w*\\+?xml.*".toRegex() + + val semicolonRegex = ";".toRegex() + + val equalsRegex = "=".toRegex() + + val spaceRegex = "\\s+".toRegex() + + val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex() + + val LFRegex = "\n".toRegex() +} diff --git a/app/src/main/java/io/legado/app/constant/BookSourceType.kt b/app/src/main/java/io/legado/app/constant/BookSourceType.kt new file mode 100755 index 000000000..fc0d4f1f0 --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/BookSourceType.kt @@ -0,0 +1,18 @@ +package io.legado.app.constant + +import androidx.annotation.IntDef + +@Suppress("ConstPropertyName") +object BookSourceType { + + const val default = 0 // 0 文本 + const val audio = 1 // 1 音频 + const val image = 2 // 2 图片 + const val file = 3 // 3 只提供下载服务的网站 + + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.SOURCE) + @IntDef(default, audio, image, file) + annotation class Type + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/BookType.kt b/app/src/main/java/io/legado/app/constant/BookType.kt new file mode 100755 index 000000000..fcdd0bd8e --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/BookType.kt @@ -0,0 +1,73 @@ +package io.legado.app.constant + +import androidx.annotation.IntDef + +/** + * 以二进制位来区分,可能一本书籍包含多个类型,每一位代表一个类型,数值为2的n次方 + * 以二进制位来区分,数据库查询更高效, 数值>=8和老版本类型区分开 + */ +@Suppress("ConstPropertyName") +object BookType { + /** + * 8 文本 + */ + const val text = 0b1000 + + /** + * 16 更新失败 + */ + const val updateError = 0b10000 + + /** + * 32 音频 + */ + const val audio = 0b100000 + + /** + * 64 图片 + */ + const val image = 0b1000000 + + /** + * 128 只提供下载服务的网站 + */ + const val webFile = 0b10000000 + + /** + * 256 本地 + */ + const val local = 0b100000000 + + /** + * 512 压缩包 表明书籍文件是从压缩包内解压来的 + */ + const val archive = 0b1000000000 + + /** + * 1024 未正式加入到书架的临时阅读书籍 + */ + const val notShelf = 0b100_0000_0000 + + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.SOURCE) + @IntDef(text, updateError, audio, image, webFile, local, archive, notShelf) + annotation class Type + + /** + * 所有可以从书源转换的书籍类型 + */ + const val allBookType = text or image or audio or webFile + + const val allBookTypeLocal = text or image or audio or webFile or local + + /** + * 本地书籍书源标志 + */ + const val localTag = "loc_book" + + /** + * 书源已webDav::开头的书籍,可以从webDav更新或重新下载 + */ + const val webDavTag = "webDav::" + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/EventBus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt new file mode 100755 index 000000000..ec0abd3fc --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -0,0 +1,37 @@ +package io.legado.app.constant + +object EventBus { + const val MEDIA_BUTTON = "mediaButton" + const val RECREATE = "RECREATE" + const val UP_BOOKSHELF = "upBookToc" + const val BOOKSHELF_REFRESH = "bookshelfRefresh" + const val ALOUD_STATE = "aloud_state" + const val TTS_PROGRESS = "ttsStart" + const val AUDIO_DS = "audioDs" + const val READ_ALOUD_DS = "readAloudDs" + const val BATTERY_CHANGED = "batteryChanged" + const val TIME_CHANGED = "timeChanged" + const val UP_CONFIG = "upConfig" + const val AUDIO_SUB_TITLE = "audioSubTitle" + const val AUDIO_STATE = "audioState" + const val AUDIO_PROGRESS = "audioProgress" + const val AUDIO_BUFFER_PROGRESS = "audioBufferProgress" + const val AUDIO_SIZE = "audioSize" + const val AUDIO_SPEED = "audioSpeed" + const val NOTIFY_MAIN = "notifyMain" + const val WEB_SERVICE = "webService" + const val UP_DOWNLOAD = "upDownload" + const val UP_DOWNLOAD_STATE = "upDownloadState" + const val SAVE_CONTENT = "saveContent" + const val CHECK_SOURCE = "checkSource" + const val CHECK_SOURCE_DONE = "checkSourceDone" + const val TIP_COLOR = "tipColor" + const val SOURCE_CHANGED = "sourceChanged" + const val SEARCH_RESULT = "searchResult" + const val UPDATE_READ_ACTION_BAR = "updateReadActionBar" + const val UP_SEEK_BAR = "upSeekBar" + const val READ_ALOUD_PLAY = "readAloudPlay" + const val EXPORT_BOOK = "exportBook" + const val UP_MANGA_CONFIG = "upMangaConfig" + const val PLAY_MODE_CHANGED = "playModeChanged" +} diff --git a/app/src/main/java/io/legado/app/constant/IntentAction.kt b/app/src/main/java/io/legado/app/constant/IntentAction.kt new file mode 100755 index 000000000..5a603fa1d --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/IntentAction.kt @@ -0,0 +1,25 @@ +package io.legado.app.constant + +@Suppress("ConstPropertyName") +object IntentAction { + const val start = "start" + const val play = "play" + const val playNew = "playNew" + const val stop = "stop" + const val resume = "resume" + const val pause = "pause" + const val addTimer = "addTimer" + const val setTimer = "setTimer" + const val prevParagraph = "prevParagraph" + const val nextParagraph = "nextParagraph" + const val upTtsSpeechRate = "upTtsSpeechRate" + const val upTtsProgress = "upTtsProgress" + const val adjustProgress = "adjustProgress" + const val adjustSpeed = "adjustSpeed" + const val prev = "prev" + const val next = "next" + const val moveTo = "moveTo" + const val init = "init" + const val remove = "remove" + const val stopPlay = "stopPlay" +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/NotificationId.kt b/app/src/main/java/io/legado/app/constant/NotificationId.kt new file mode 100755 index 000000000..377e3c13e --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/NotificationId.kt @@ -0,0 +1,19 @@ +package io.legado.app.constant + +/** + * 通知ID不能重复,统一规划通知ID + */ +@Suppress("ConstPropertyName") +object NotificationId { + + const val ReadAloudService = 101 + const val AudioPlayService = 102 + const val CacheBookService = 103 + const val ExportBookService = 104 + const val WebService = 105 + const val DownloadService = 106 + const val CheckSourceService = 107 + const val Download = 10000 + const val ExportBook = 201 + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/PageAnim.kt b/app/src/main/java/io/legado/app/constant/PageAnim.kt new file mode 100755 index 000000000..ec505b455 --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/PageAnim.kt @@ -0,0 +1,23 @@ +package io.legado.app.constant + +import androidx.annotation.IntDef + +@Suppress("ConstPropertyName") +object PageAnim { + + const val coverPageAnim = 0 + + const val slidePageAnim = 1 + + const val simulationPageAnim = 2 + + const val scrollPageAnim = 3 + + const val noAnim = 4 + + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.SOURCE) + @IntDef(coverPageAnim, slidePageAnim, simulationPageAnim, scrollPageAnim, noAnim) + annotation class Anim + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt new file mode 100755 index 000000000..a45ed0dc7 --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -0,0 +1,189 @@ +package io.legado.app.constant + +@Suppress("ConstPropertyName") +object PreferKey { + const val language = "language" + const val fontScale = "fontScale" + const val themeMode = "themeMode" + const val userAgent = "userAgent" + const val showUnread = "showUnread" + const val bookGroupStyle = "bookGroupStyle" + const val useDefaultCover = "useDefaultCover" + const val loadCoverOnlyWifi = "loadCoverOnlyWifi" + const val coverShowName = "coverShowName" + const val coverShowAuthor = "coverShowAuthor" + const val coverShowNameN = "coverShowNameN" + const val coverShowAuthorN = "coverShowAuthorN" + const val remoteServerId = "remoteServerId" + const val hideStatusBar = "hideStatusBar" + const val clickActionTL = "clickActionTopLeft" + const val clickActionTC = "clickActionTopCenter" + const val clickActionTR = "clickActionTopRight" + const val clickActionML = "clickActionMiddleLeft" + const val clickActionMC = "clickActionMiddleCenter" + const val clickActionMR = "clickActionMiddleRight" + const val clickActionBL = "clickActionBottomLeft" + const val clickActionBC = "clickActionBottomCenter" + const val clickActionBR = "clickActionBottomRight" + const val hideNavigationBar = "hideNavigationBar" + const val precisionSearch = "precisionSearch" + const val readAloudByPage = "readAloudByPage" + const val ttsEngine = "appTtsEngine" + const val ttsFollowSys = "ttsFollowSys" + const val ttsSpeechRate = "ttsSpeechRate" + const val prevKeys = "prevKeyCodes" + const val nextKeys = "nextKeyCodes" + const val showDiscovery = "showDiscovery" + const val enableReview = "enableReview" + const val showRss = "showRss" + const val bookshelfLayout = "bookshelfLayout" + const val bookshelfSort = "bookshelfSort" + const val bookExportFileName = "bookExportFileName" + const val bookImportFileName = "bookImportFileName" + const val episodeExportFileName = "episodeExportFileName" + const val recordLog = "recordLog" + const val processText = "process_text" + const val cleanCache = "cleanCache" + const val saveTabPosition = "saveTabPosition" + const val fontFolder = "fontFolder" + const val backupPath = "backupUri" + const val restoreIgnore = "restoreIgnore" + const val threadCount = "threadCount" + const val webPort = "webPort" + const val keepLight = "keep_light" + const val webService = "webService" + const val webDavUrl = "web_dav_url" + const val webDavAccount = "web_dav_account" + const val webDavPassword = "web_dav_password" + const val webDavDir = "webDavDir" + const val enableCustomExport = "enableCustomExport" + const val exportToWebDav = "webDavCacheBackup" + const val exportNoChapterName = "exportNoChapterName" + const val exportType = "exportType" + const val exportPictureFile = "exportPictureFile" + const val changeSourceCheckAuthor = "changeSourceCheckAuthor" + const val changeSourceLoadToc = "changeSourceLoadToc" + const val changeSourceLoadInfo = "changeSourceLoadInfo" + const val changeSourceLoadWordCount = "changeSourceLoadWordCount" + const val chineseConverterType = "chineseConverterType" + const val launcherIcon = "launcherIcon" + const val textSelectAble = "selectText" + const val shareLayout = "shareLayout" + const val comicStyleSelect = "comicStyleSelect" + const val readStyleSelect = "readStyleSelect" + const val systemTypefaces = "system_typefaces" + const val readBodyToLh = "readBodyToLh" + const val textFullJustify = "textFullJustify" + const val textBottomJustify = "textBottomJustify" + const val autoReadSpeed = "autoReadSpeed" + const val barElevation = "barElevation" + const val transparentStatusBar = "transparentStatusBar" + const val immNavigationBar = "immNavigationBar" + const val defaultCover = "defaultCover" + const val defaultCoverDark = "defaultCoverDark" + const val replaceEnableDefault = "replaceEnableDefault" + const val showBrightnessView = "showBrightnessView" + const val autoClearExpired = "autoClearExpired" + const val autoChangeSource = "autoChangeSource" + const val importKeepName = "importKeepName" + const val importKeepGroup = "importKeepGroup" + const val screenOrientation = "screenOrientation" + const val syncBookProgress = "syncBookProgress" + const val syncBookProgressPlus = "syncBookProgressPlus" + const val cronet = "Cronet" + const val antiAlias = "antiAlias" + const val bitmapCacheSize = "bitmapCacheSize" + const val imageRetainNum = "imageRetainNum" + const val preDownloadNum = "preDownloadNum" + const val mangaPreDownloadNum = "mangaPreDownloadNum" + const val mangaAutoPageSpeed = "mangaAutoPageSpeed" + const val mangaFooterConfig = "mangaFooterConfig" + const val disableClickScroll = "disableClickScroll" + const val enableMangaHorizontalScroll = "enableMangaHorizontalScroll" + const val hideMangaTitle = "hideMangaTitle" + const val mangaColorFilter = "mangaColorFilter" + const val enableMangaEInk = "enableMangaEInk" + const val mangaEInkThreshold = "mangaEInkThreshold" + const val disableHorizontalPageSnap = "disableHorizontalPageSnap" + const val enableMangaGray = "enableMangaGray" + const val autoRefresh = "auto_refresh" + const val defaultToRead = "defaultToRead" + const val exportCharset = "exportCharset" + const val exportUseReplace = "exportUseReplace" + const val useZhLayout = "useZhLayout" + const val brightness = "brightness" + const val nightBrightness = "nightBrightness" + const val expandTextMenu = "expandTextMenu" + const val doublePageHorizontal = "doubleHorizontalPage" + const val readUrlOpenInBrowser = "readUrlInBrowser" + const val defaultBookTreeUri = "defaultBookTreeUri" + const val checkSource = "checkSource" + const val uploadRule = "uploadRule" + const val tocUiUseReplace = "tocUiUseReplace" + const val tocCountWords = "tocCountWords" + const val enableReadRecord = "enableReadRecord" + const val localBookImportSort = "localBookImportSort" + const val customWelcome = "customWelcome" + const val welcomeImage = "welcomeImagePath" + const val welcomeImageDark = "welcomeImagePathDark" + const val welcomeShowText = "welcomeShowText" + const val welcomeShowTextDark = "welcomeShowTextDark" + const val welcomeShowIcon = "welcomeShowIcon" + const val welcomeShowIconDark = "welcomeShowIconDark" + const val pageTouchSlop = "pageTouchSlop" + const val showAddToShelfAlert = "showAddToShelfAlert" + const val ignoreAudioFocus = "ignoreAudioFocus" + const val parallelExportBook = "parallelExportBook" + const val progressBarBehavior = "progressBarBehavior" + const val sourceEditMaxLine = "sourceEditMaxLine" + const val ttsTimer = "ttsTimer" + const val noAnimScrollPage = "noAnimScrollPage" + const val webDavDeviceName = "webDavDeviceName" + const val webServiceWakeLock = "webServiceWakeLock" + const val audioPlayWakeLock = "audioPlayWakeLock" + const val readAloudWakeLock = "readAloudWakeLock" + const val showLastUpdateTime = "showLastUpdateTime" + const val showWaitUpCount = "showWaitUpCount" + const val clearWebViewData = "clearWebViewData" + const val onlyLatestBackup = "onlyLatestBackup" + const val brightnessVwPos = "brightnessVwPos" + const val shrinkDatabase = "shrinkDatabase" + const val batchChangeSourceDelay = "batchChangeSourceDelay" + const val openBookInfoByClickTitle = "openBookInfoByClickTitle" + const val defaultHomePage = "defaultHomePage" + const val showBookshelfFastScroller = "showBookshelfFastScroller" + const val importKeepEnable = "importKeepEnable" + const val previewImageByClick = "previewImageByClick" + const val keyPageOnLongPress = "keyPageOnLongPress" + const val volumeKeyPage = "volumeKeyPage" + const val volumeKeyPageOnPlay = "volumeKeyPageOnPlay" + const val mouseWheelPage = "mouseWheelPage" + const val recordHeapDump = "recordHeapDump" + const val optimizeRender = "optimizeRender" + const val updateToVariant = "updateToVariant" + const val streamReadAloudAudio = "streamReadAloudAudio" + const val pauseReadAloudWhilePhoneCalls = "pauseReadAloudWhilePhoneCalls" + const val readAloudByMediaButton = "readAloudByMediaButton" + const val showMangaUi = "showMangaUi" + const val disableMangaScale = "disableMangaScale" + const val disableMangaPageAnim = "disableMangaPageAnim" + const val paddingDisplayCutouts = "paddingDisplayCutouts" + const val autoCheckNewBackup = "autoCheckNewBackup" + + const val cPrimary = "colorPrimary" + const val cAccent = "colorAccent" + const val cBackground = "colorBackground" + const val cBBackground = "colorBottomBackground" + const val bgImage = "backgroundImage" + const val bgImageBlurring = "backgroundImageBlurring" + + const val cNPrimary = "colorPrimaryNight" + const val cNAccent = "colorAccentNight" + const val cNBackground = "colorBackgroundNight" + const val cNBBackground = "colorBottomBackgroundNight" + const val bgImageN = "backgroundImageNight" + const val bgImageNBlurring = "backgroundImageNightBlurring" + const val showReadTitleAddition = "showReadTitleAddition" + const val readBarStyleFollowPage = "readBarStyleFollowPage" + const val contentSelectSpeakMod = "contentReadAloudMod" +} diff --git a/app/src/main/java/io/legado/app/constant/SourceType.kt b/app/src/main/java/io/legado/app/constant/SourceType.kt new file mode 100755 index 000000000..14252146c --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/SourceType.kt @@ -0,0 +1,16 @@ +package io.legado.app.constant + +import androidx.annotation.IntDef + +@Suppress("ConstPropertyName") +object SourceType { + + const val book = 0 + const val rss = 1 + + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.SOURCE) + @IntDef(book, rss) + annotation class Type + +} diff --git a/app/src/main/java/io/legado/app/constant/Status.kt b/app/src/main/java/io/legado/app/constant/Status.kt new file mode 100755 index 000000000..678c62221 --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/Status.kt @@ -0,0 +1,7 @@ +package io.legado.app.constant + +object Status { + const val STOP = 0 + const val PLAY = 1 + const val PAUSE = 3 +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/Theme.kt b/app/src/main/java/io/legado/app/constant/Theme.kt new file mode 100755 index 000000000..c5f063bee --- /dev/null +++ b/app/src/main/java/io/legado/app/constant/Theme.kt @@ -0,0 +1,5 @@ +package io.legado.app.constant + +enum class Theme { + Dark, Light, Auto, Transparent, EInk; +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt new file mode 100755 index 000000000..b9254ed1f --- /dev/null +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -0,0 +1,244 @@ +package io.legado.app.data + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import android.os.Build +import android.util.Log +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.sqlite.db.SupportSQLiteDatabase +import io.legado.app.data.dao.BookChapterDao +import io.legado.app.data.dao.BookDao +import io.legado.app.data.dao.BookGroupDao +import io.legado.app.data.dao.BookSourceDao +import io.legado.app.data.dao.BookmarkDao +import io.legado.app.data.dao.CacheDao +import io.legado.app.data.dao.CookieDao +import io.legado.app.data.dao.DictRuleDao +import io.legado.app.data.dao.HttpTTSDao +import io.legado.app.data.dao.KeyboardAssistsDao +import io.legado.app.data.dao.ReadRecordDao +import io.legado.app.data.dao.ReplaceRuleDao +import io.legado.app.data.dao.RssArticleDao +import io.legado.app.data.dao.RssReadRecordDao +import io.legado.app.data.dao.RssSourceDao +import io.legado.app.data.dao.RssStarDao +import io.legado.app.data.dao.RuleSubDao +import io.legado.app.data.dao.SearchBookDao +import io.legado.app.data.dao.SearchKeywordDao +import io.legado.app.data.dao.ServerDao +import io.legado.app.data.dao.TxtTocRuleDao +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookGroup +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.Bookmark +import io.legado.app.data.entities.Cache +import io.legado.app.data.entities.Cookie +import io.legado.app.data.entities.DictRule +import io.legado.app.data.entities.HttpTTS +import io.legado.app.data.entities.KeyboardAssist +import io.legado.app.data.entities.ReadRecord +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssReadRecord +import io.legado.app.data.entities.RssSource +import io.legado.app.data.entities.RssStar +import io.legado.app.data.entities.RuleSub +import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.SearchKeyword +import io.legado.app.data.entities.Server +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.DefaultData +import org.intellij.lang.annotations.Language +import splitties.init.appCtx +import java.util.Locale + +val appDb by lazy { + Room.databaseBuilder(appCtx, AppDatabase::class.java, AppDatabase.DATABASE_NAME) + .fallbackToDestructiveMigrationFrom(false, 1, 2, 3, 4, 5, 6, 7, 8, 9) + .addMigrations(*DatabaseMigrations.migrations) + .allowMainThreadQueries() + .addCallback(AppDatabase.dbCallback) + .build() +} + +@Database( + version = 75, + exportSchema = true, + entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, + ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, + RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, + RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, + RuleSub::class, DictRule::class, KeyboardAssist::class, Server::class], + views = [BookSourcePart::class], + autoMigrations = [ + AutoMigration(from = 43, to = 44), + AutoMigration(from = 44, to = 45), + AutoMigration(from = 45, to = 46), + AutoMigration(from = 46, to = 47), + AutoMigration(from = 47, to = 48), + AutoMigration(from = 48, to = 49), + AutoMigration(from = 49, to = 50), + AutoMigration(from = 50, to = 51), + AutoMigration(from = 51, to = 52), + AutoMigration(from = 52, to = 53), + AutoMigration(from = 53, to = 54), + AutoMigration(from = 54, to = 55, spec = DatabaseMigrations.Migration_54_55::class), + AutoMigration(from = 55, to = 56), + AutoMigration(from = 56, to = 57), + AutoMigration(from = 57, to = 58), + AutoMigration(from = 58, to = 59), + AutoMigration(from = 59, to = 60), + AutoMigration(from = 60, to = 61), + AutoMigration(from = 61, to = 62), + AutoMigration(from = 62, to = 63), + AutoMigration(from = 63, to = 64), + AutoMigration(from = 64, to = 65, spec = DatabaseMigrations.Migration_64_65::class), + AutoMigration(from = 65, to = 66), + AutoMigration(from = 66, to = 67), + AutoMigration(from = 67, to = 68), + AutoMigration(from = 68, to = 69), + AutoMigration(from = 69, to = 70), + AutoMigration(from = 70, to = 71), + AutoMigration(from = 71, to = 72), + AutoMigration(from = 72, to = 73), + AutoMigration(from = 73, to = 74), + AutoMigration(from = 74, to = 75), + ] +) +abstract class AppDatabase : RoomDatabase() { + + abstract val bookDao: BookDao + abstract val bookGroupDao: BookGroupDao + abstract val bookSourceDao: BookSourceDao + abstract val bookChapterDao: BookChapterDao + abstract val replaceRuleDao: ReplaceRuleDao + abstract val searchBookDao: SearchBookDao + abstract val searchKeywordDao: SearchKeywordDao + abstract val rssSourceDao: RssSourceDao + abstract val bookmarkDao: BookmarkDao + abstract val rssArticleDao: RssArticleDao + abstract val rssStarDao: RssStarDao + abstract val rssReadRecordDao: RssReadRecordDao + abstract val cookieDao: CookieDao + abstract val txtTocRuleDao: TxtTocRuleDao + abstract val readRecordDao: ReadRecordDao + abstract val httpTTSDao: HttpTTSDao + abstract val cacheDao: CacheDao + abstract val ruleSubDao: RuleSubDao + abstract val dictRuleDao: DictRuleDao + abstract val keyboardAssistsDao: KeyboardAssistsDao + abstract val serverDao: ServerDao + + companion object { + + const val DATABASE_NAME = "legado.db" + + const val BOOK_TABLE_NAME = "books" + const val BOOK_SOURCE_TABLE_NAME = "book_sources" + const val RSS_SOURCE_TABLE_NAME = "rssSources" + + val dbCallback = object : Callback() { + + override fun onCreate(db: SupportSQLiteDatabase) { + // 只在 API 级别 23 (Marshmallow) 及以上版本尝试设置区域设置 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + Log.d("AppDatabaseCallback", "准备 设置 locale for API ${Build.VERSION.SDK_INT}...") + db.setLocale(Locale.CHINESE) + // 在 21 上报错,但无法拦截 + Log.d("AppDatabaseCallback", "成功 设置 locale for API ${Build.VERSION.SDK_INT}.") + } catch (e: Exception) { + Log.e("AppDatabaseCallback", "错误 设置 locale in onCreate for API ${Build.VERSION.SDK_INT}", e) + } + } else { + Log.i("AppDatabaseCallback", "跳过 setLocale for API ${Build.VERSION.SDK_INT} (below M).") + } + } + + override fun onOpen(db: SupportSQLiteDatabase) { + @Language("sql") + val insertBookGroupAllSql = """ + insert into book_groups(groupId, groupName, 'order', show) + select ${BookGroup.IdAll}, '全部', -10, 1 + where not exists (select * from book_groups where groupId = ${BookGroup.IdAll}) + """.trimIndent() + db.execSQL(insertBookGroupAllSql) + @Language("sql") + val insertBookGroupLocalSql = """ + insert into book_groups(groupId, groupName, 'order', enableRefresh, show) + select ${BookGroup.IdLocal}, '本地', -9, 0, 1 + where not exists (select * from book_groups where groupId = ${BookGroup.IdLocal}) + """.trimIndent() + db.execSQL(insertBookGroupLocalSql) + @Language("sql") + val insertBookGroupMusicSql = """ + insert into book_groups(groupId, groupName, 'order', show) + select ${BookGroup.IdAudio}, '音频', -8, 1 + where not exists (select * from book_groups where groupId = ${BookGroup.IdAudio}) + """.trimIndent() + db.execSQL(insertBookGroupMusicSql) + @Language("sql") + val insertBookGroupNetNoneGroupSql = """ + insert into book_groups(groupId, groupName, 'order', show) + select ${BookGroup.IdNetNone}, '网络未分组', -7, 1 + where not exists (select * from book_groups where groupId = ${BookGroup.IdNetNone}) + """.trimIndent() + db.execSQL(insertBookGroupNetNoneGroupSql) + @Language("sql") + val insertBookGroupLocalNoneGroupSql = """ + insert into book_groups(groupId, groupName, 'order', show) + select ${BookGroup.IdLocalNone}, '本地未分组', -6, 0 + where not exists (select * from book_groups where groupId = ${BookGroup.IdLocalNone}) + """.trimIndent() + db.execSQL(insertBookGroupLocalNoneGroupSql) + @Language("sql") + val insertBookGroupErrorSql = """ + insert into book_groups(groupId, groupName, 'order', show) + select ${BookGroup.IdError}, '更新失败', -1, 1 + where not exists (select * from book_groups where groupId = ${BookGroup.IdError}) + """.trimIndent() + db.execSQL(insertBookGroupErrorSql) + @Language("sql") + val upBookSourceLoginUiSql = + "update book_sources set loginUi = null where loginUi = 'null'" + db.execSQL(upBookSourceLoginUiSql) + @Language("sql") + val upRssSourceLoginUiSql = + "update rssSources set loginUi = null where loginUi = 'null'" + db.execSQL(upRssSourceLoginUiSql) + @Language("sql") + val upHttpTtsLoginUiSql = + "update httpTTS set loginUi = null where loginUi = 'null'" + db.execSQL(upHttpTtsLoginUiSql) + @Language("sql") + val upHttpTtsConcurrentRateSql = + "update httpTTS set concurrentRate = '0' where concurrentRate is null" + db.execSQL(upHttpTtsConcurrentRateSql) + db.query("select * from keyboardAssists order by serialNo").use { + if (it.count == 0) { + DefaultData.keyboardAssists.forEach { keyboardAssist -> + val contentValues = ContentValues().apply { + put("type", keyboardAssist.type) + put("key", keyboardAssist.key) + put("value", keyboardAssist.value) + put("serialNo", keyboardAssist.serialNo) + } + db.insert( + "keyboardAssists", + SQLiteDatabase.CONFLICT_REPLACE, + contentValues + ) + } + } + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt b/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt new file mode 100755 index 000000000..415d20ed6 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt @@ -0,0 +1,374 @@ +package io.legado.app.data + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import io.legado.app.constant.AppConst +import io.legado.app.constant.BookSourceType +import io.legado.app.constant.BookType + +object DatabaseMigrations { + + val migrations: Array by lazy { + arrayOf( + migration_10_11, migration_11_12, migration_12_13, migration_13_14, + migration_14_15, migration_15_17, migration_17_18, migration_18_19, + migration_19_20, migration_20_21, migration_21_22, migration_22_23, + migration_23_24, migration_24_25, migration_25_26, migration_26_27, + migration_27_28, migration_28_29, migration_29_30, migration_30_31, + migration_31_32, migration_32_33, migration_33_34, migration_34_35, + migration_35_36, migration_36_37, migration_37_38, migration_38_39, + migration_39_40, migration_40_41, migration_41_42, migration_42_43, + ) + } + + private val migration_10_11 = object : Migration(10, 11) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE txtTocRules") + db.execSQL( + """CREATE TABLE txtTocRules(id INTEGER NOT NULL, + name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL, + enable INTEGER NOT NULL, PRIMARY KEY (id))""" + ) + } + } + + private val migration_11_12 = object : Migration(11, 12) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE rssSources ADD style TEXT ") + } + } + + private val migration_12_13 = object : Migration(12, 13) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ") + } + } + + private val migration_13_14 = object : Migration(13, 14) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, + `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, + `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, + `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, + `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, + `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, + `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))""" + ) + db.execSQL("INSERT INTO books_new select * from books ") + db.execSQL("DROP TABLE books") + db.execSQL("ALTER TABLE books_new RENAME TO books") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ") + } + } + + private val migration_14_15 = object : Migration(14, 15) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''") + } + } + + private val migration_15_17 = object : Migration(15, 17) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))") + } + } + + private val migration_17_18 = object : Migration(17, 18) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))") + } + } + + private val migration_18_19 = object : Migration(18, 19) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, + PRIMARY KEY(`androidId`, `bookName`))""" + ) + db.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${AppConst.androidId}' as androidId, bookName, readTime from readRecord") + db.execSQL("DROP TABLE readRecord") + db.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord") + } + } + private val migration_19_20 = object : Migration(19, 20) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT") + } + } + + private val migration_20_21 = object : Migration(20, 21) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1") + } + } + + private val migration_21_22 = object : Migration(21, 22) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, + `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, + `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, + `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, + `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, + `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, + `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))""" + ) + db.execSQL( + """INSERT INTO books_new select `bookUrl`, `tocUrl`, `origin`, `originName`, `name`, `author`, `kind`, `customTag`, `coverUrl`, + `customCoverUrl`, `intro`, `customIntro`, `charset`, `type`, `group`, `latestChapterTitle`, `latestChapterTime`, `lastCheckTime`, + `lastCheckCount`, `totalChapterNum`, `durChapterTitle`, `durChapterIndex`, `durChapterPos`, `durChapterTime`, `wordCount`, `canUpdate`, + `order`, `originOrder`, `variable`, null + from books""" + ) + db.execSQL("DROP TABLE books") + db.execSQL("ALTER TABLE books_new RENAME TO books") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ") + } + } + + private val migration_22_23 = object : Migration(22, 23) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''") + } + } + + private val migration_23_24 = object : Migration(23, 24) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)") + } + } + + private val migration_24_25 = object : Migration(24, 25) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """CREATE TABLE IF NOT EXISTS `sourceSubs` + (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, + PRIMARY KEY(`id`))""" + ) + } + } + + private val migration_25_26 = object : Migration(25, 26) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, + `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))""" + ) + db.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ") + db.execSQL("DROP TABLE `sourceSubs`") + } + } + + private val migration_26_27 = object : Migration(26, 27) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ") + db.execSQL( + """CREATE TABLE IF NOT EXISTS `bookmarks1` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, + `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, + `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))""" + ) + db.execSQL( + """insert into `bookmarks1` + select `time`, `bookUrl`, `bookName`, `bookAuthor`, `chapterIndex`, `pageIndex`, `chapterName`, '', `content` + from bookmarks""" + ) + db.execSQL(" DROP TABLE `bookmarks` ") + db.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)") + } + } + + private val migration_27_28 = object : Migration(27, 28) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE rssArticles ADD variable TEXT") + db.execSQL("ALTER TABLE rssStars ADD variable TEXT") + } + } + + private val migration_28_29 = object : Migration(28, 29) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT") + } + } + + private val migration_29_30 = object : Migration(29, 30) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT") + db.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `epubChapters` + (`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT, + PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE ) + """ + ) + db.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)") + } + } + + private val migration_30_31 = object : Migration(30, 31) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE readRecord RENAME TO readRecord1") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`)) + """ + ) + db.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1") + } + } + + private val migration_31_32 = object : Migration(31, 32) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("DROP TABLE `epubChapters`") + } + } + + private val migration_32_33 = object : Migration(32, 33) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL, + `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, + `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, + `content` TEXT NOT NULL, PRIMARY KEY(`time`)) + """ + ) + db.execSQL( + """ + CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`) + """ + ) + db.execSQL( + """ + insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content) + select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor, + chapterIndex, chapterPos, chapterName, bookText, content from bookmarks_old o + left join books b on o.bookUrl = b.bookUrl + """ + ) + } + } + + private val migration_33_34 = object : Migration(33, 34) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT") + } + } + + private val migration_34_35 = object : Migration(34, 35) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `book_sources` ADD `concurrentRate` TEXT") + } + } + + private val migration_35_36 = object : Migration(35, 36) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `book_sources` ADD `loginUi` TEXT") + db.execSQL("ALTER TABLE `book_sources` ADD`loginCheckJs` TEXT") + } + } + + private val migration_36_37 = object : Migration(36, 37) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `rssSources` ADD `loginUrl` TEXT") + db.execSQL("ALTER TABLE `rssSources` ADD `loginUi` TEXT") + db.execSQL("ALTER TABLE `rssSources` ADD `loginCheckJs` TEXT") + } + } + + private val migration_37_38 = object : Migration(37, 38) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `book_sources` ADD `respondTime` INTEGER NOT NULL DEFAULT 180000") + } + } + + private val migration_38_39 = object : Migration(38, 39) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `rssSources` ADD `concurrentRate` TEXT") + } + } + + private val migration_39_40 = object : Migration(39, 40) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `chapters` ADD `isVip` INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE `chapters` ADD `isPay` INTEGER NOT NULL DEFAULT 0") + } + } + + private val migration_40_41 = object : Migration(40, 41) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `httpTTS` ADD `loginUrl` TEXT") + db.execSQL("ALTER TABLE `httpTTS` ADD `loginUi` TEXT") + db.execSQL("ALTER TABLE `httpTTS` ADD `loginCheckJs` TEXT") + db.execSQL("ALTER TABLE `httpTTS` ADD `header` TEXT") + db.execSQL("ALTER TABLE `httpTTS` ADD `concurrentRate` TEXT") + } + } + + private val migration_41_42 = object : Migration(41, 42) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE 'httpTTS' ADD `contentType` TEXT") + } + } + + private val migration_42_43 = object : Migration(42, 43) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `chapters` ADD `isVolume` INTEGER NOT NULL DEFAULT 0") + } + } + + + @Suppress("ClassName") + class Migration_54_55 : AutoMigrationSpec { + + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + update books set type = ${BookType.audio} + where type = ${BookSourceType.audio} + """.trimIndent() + ) + db.execSQL( + """ + update books set type = ${BookType.image} + where type = ${BookSourceType.image} + """.trimIndent() + ) + db.execSQL( + """ + update books set type = ${BookType.webFile} + where type = ${BookSourceType.file} + """.trimIndent() + ) + db.execSQL( + """ + update books set type = ${BookType.text} + where type = ${BookSourceType.default} + """.trimIndent() + ) + db.execSQL( + """ + update books set type = type | ${BookType.local} + where origin like '${BookType.localTag}%' or origin like '${BookType.webDavTag}%' + """.trimIndent() + ) + } + + } + + + @Suppress("ClassName") + @DeleteColumn( + tableName = "book_sources", + columnName = "enabledReview" + ) + class Migration_64_65 : AutoMigrationSpec + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/README.md b/app/src/main/java/io/legado/app/data/README.md new file mode 100755 index 000000000..68dc6840a --- /dev/null +++ b/app/src/main/java/io/legado/app/data/README.md @@ -0,0 +1,18 @@ +# 存储数据用 + +* dao 数据操作 +* entities 数据模型 +* \Book 书籍信息 +* \BookChapter 目录信息 +* \BookGroup 书籍分组 +* \Bookmark 书签 +* \BookSource 书源 +* \Cookie http cookie +* \ReplaceRule 替换规则 +* \RssArticle rss条目 +* \RssReadRecord rss阅读记录 +* \RssSource rss源 +* \RssStar rss收藏 +* \SearchBook 搜索结果 +* \SearchKeyword 搜索关键字 +* \TxtTocRule txt文件目录规则 diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt new file mode 100755 index 000000000..87336219b --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt @@ -0,0 +1,46 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import io.legado.app.data.entities.BookChapter + +@Dao +interface BookChapterDao { + + @Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%' order by `index`") + fun search(bookUrl: String, key: String): List + + @Query("SELECT * FROM chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end and title like '%'||:key||'%' order by `index`") + fun search(bookUrl: String, key: String, start: Int, end: Int): List + + @Query("select * from chapters where bookUrl = :bookUrl order by `index`") + fun getChapterList(bookUrl: String): List + + @Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end order by `index`") + fun getChapterList(bookUrl: String, start: Int, end: Int): List + + @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") + fun getChapter(bookUrl: String, index: Int): BookChapter? + + @Query("select * from chapters where bookUrl = :bookUrl and `title` = :title") + fun getChapter(bookUrl: String, title: String): BookChapter? + + @Query("select count(url) from chapters where bookUrl = :bookUrl") + fun getChapterCount(bookUrl: String): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg bookChapter: BookChapter) + + @Update + fun update(vararg bookChapter: BookChapter) + + @Query("delete from chapters where bookUrl = :bookUrl") + fun delByBook(bookUrl: String) + + @Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url") + fun upWordCount(bookUrl: String, url: String, wordCount: String) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt new file mode 100755 index 000000000..e2cec7bd3 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt @@ -0,0 +1,171 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.data.entities.BookSource +import io.legado.app.help.book.isNotShelf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@Dao +interface BookDao { + + fun flowByGroup(groupId: Long): Flow> { + return when (groupId) { + BookGroup.IdRoot -> flowRoot() + BookGroup.IdAll -> flowAll() + BookGroup.IdLocal -> flowLocal() + BookGroup.IdAudio -> flowAudio() + BookGroup.IdNetNone -> flowNetNoGroup() + BookGroup.IdLocalNone -> flowLocalNoGroup() + BookGroup.IdError -> flowUpdateError() + else -> flowByUserGroup(groupId) + }.map { list -> + list.filterNot { it.isNotShelf } + } + } + + @Query( + """ + select * from books where type & ${BookType.text} > 0 + and type & ${BookType.local} = 0 + and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0 + and (select show from book_groups where groupId = ${BookGroup.IdNetNone}) != 1 + """ + ) + fun flowRoot(): Flow> + + @Query("SELECT * FROM books order by durChapterTime desc") + fun flowAll(): Flow> + + @Query("SELECT * FROM books WHERE type & ${BookType.audio} > 0") + fun flowAudio(): Flow> + + @Query("SELECT * FROM books WHERE type & ${BookType.local} > 0") + fun flowLocal(): Flow> + + @Query( + """ + select * from books where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0 + and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0 + """ + ) + fun flowNetNoGroup(): Flow> + + @Query( + """ + select * from books where type & ${BookType.local} > 0 + and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0 + """ + ) + fun flowLocalNoGroup(): Flow> + + @Query("SELECT * FROM books WHERE (`group` & :group) > 0") + fun flowByUserGroup(group: Long): Flow> + + @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") + fun flowSearch(key: String): Flow> + + @Query("SELECT * FROM books where type & ${BookType.updateError} > 0 order by durChapterTime desc") + fun flowUpdateError(): Flow> + + @Query("SELECT * FROM books WHERE (`group` & :group) > 0") + fun getBooksByGroup(group: Long): List + + @Query("SELECT * FROM books WHERE `name` in (:names)") + fun findByName(vararg names: String): List + + @Query("select * from books where originName = :fileName") + fun getBookByFileName(fileName: String): Book? + + @Query("SELECT * FROM books WHERE bookUrl = :bookUrl") + fun getBook(bookUrl: String): Book? + + @Query("SELECT * FROM books WHERE name = :name and author = :author") + fun getBook(name: String, author: String): Book? + + @Query("""select distinct bs.* from books, book_sources bs + where origin == bookSourceUrl and origin not like '${BookType.localTag}%' + and origin not like '${BookType.webDavTag}%'""") + fun getAllUseBookSource(): List + + @Query("SELECT * FROM books WHERE name = :name and origin = :origin") + fun getBookByOrigin(name: String, origin: String): Book? + + @get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups)") + val noGroupSize: Int + + @get:Query("SELECT * FROM books where type & ${BookType.local} = 0") + val webBooks: List + + @get:Query("SELECT * FROM books where type & ${BookType.local} = 0 and canUpdate = 1") + val hasUpdateBooks: List + + @get:Query("SELECT * FROM books") + val all: List + + @Query("SELECT * FROM books where type & :type > 0 and type & ${BookType.local} = 0") + fun getByTypeOnLine(type: Int): List + + @get:Query("SELECT * FROM books where type & ${BookType.text} > 0 ORDER BY durChapterTime DESC limit 1") + val lastReadBook: Book? + + @get:Query("SELECT bookUrl FROM books") + val allBookUrls: List + + @get:Query("SELECT COUNT(*) FROM books") + val allBookCount: Int + + @get:Query("select min(`order`) from books") + val minOrder: Int + + @get:Query("select max(`order`) from books") + val maxOrder: Int + + @Query("select exists(select 1 from books where bookUrl = :bookUrl)") + fun has(bookUrl: String): Boolean + + @Query("select exists(select 1 from books where name = :name and author = :author)") + fun has(name: String, author: String): Boolean + + @Query( + """select exists(select 1 from books where type & ${BookType.local} > 0 + and (originName = :fileName or (origin != '${BookType.localTag}' and origin like '%' || :fileName)))""" + ) + fun hasFile(fileName: String): Boolean + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg book: Book) + + @Update + fun update(vararg book: Book) + + @Delete + fun delete(vararg book: Book) + + @Transaction + fun replace(oldBook: Book, newBook: Book) { + delete(oldBook) + insert(newBook) + } + + @Query("update books set durChapterPos = :pos where bookUrl = :bookUrl") + fun upProgress(bookUrl: String, pos: Int) + + @Query("update books set `group` = :newGroupId where `group` = :oldGroupId") + fun upGroup(oldGroupId: Long, newGroupId: Long) + + @Query("update books set `group` = `group` - :group where `group` & :group > 0") + fun removeGroup(group: Long) + + @Query("delete from books where type & ${BookType.notShelf} > 0") + fun deleteNotShelfBook() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt new file mode 100755 index 000000000..724cae6f9 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt @@ -0,0 +1,103 @@ +package io.legado.app.data.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import io.legado.app.constant.BookType +import io.legado.app.data.entities.BookGroup +import kotlinx.coroutines.flow.Flow + +@Dao +interface BookGroupDao { + + @Query("select * from book_groups where groupId = :id") + fun getByID(id: Long): BookGroup? + + @Query("select * from book_groups where groupName = :groupName") + fun getByName(groupName: String): BookGroup? + + @Query("SELECT * FROM book_groups ORDER BY `order`") + fun flowAll(): Flow> + + @get:Query( + """ + with const as (SELECT sum(groupId) sumGroupId FROM book_groups where groupId > 0) + SELECT book_groups.* FROM book_groups join const + where show > 0 + and ( + (groupId >= 0 and exists (select 1 from books where `group` & book_groups.groupId > 0)) + or groupId = -1 + or (groupId = -2 and exists (select 1 from books where type & ${BookType.local} > 0)) + or (groupId = -3 and exists (select 1 from books where type & ${BookType.audio} > 0)) + or (groupId = -11 and exists (select 1 from books where type & ${BookType.updateError} > 0)) + or (groupId = -4 + and exists ( + select 1 from books + where type & ${BookType.audio} = 0 + and type & ${BookType.local} = 0 + and const.sumGroupId & `group` = 0 + ) + ) + or (groupId = -5 + and exists ( + select 1 from books + where type & ${BookType.audio} = 0 + and type & ${BookType.local} > 0 + and const.sumGroupId & `group` = 0 + ) + ) + ) + ORDER BY `order`""" + ) + val show: LiveData> + + @Query("SELECT * FROM book_groups where groupId >= 0 ORDER BY `order`") + fun flowSelect(): Flow> + + @get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0") + val idsSum: Long + + @get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0") + val maxOrder: Int + + @get:Query("SELECT * FROM book_groups ORDER BY `order`") + val all: List + + @get:Query("select count(*) < 64 from book_groups where groupId >= 0 or groupId == ${Long.MIN_VALUE}") + val canAddGroup: Boolean + + @Query("update book_groups set show = 1 where groupId = :groupId") + fun enableGroup(groupId: Long) + + @Query("select groupName from book_groups where groupId > 0 and (groupId & :id) > 0") + fun getGroupNames(id: Long): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg bookGroup: BookGroup) + + @Update + fun update(vararg bookGroup: BookGroup) + + @Delete + fun delete(vararg bookGroup: BookGroup) + + fun isInRules(id: Long): Boolean { + if (id < 0) { + return true + } + return id and (id - 1) == 0L + } + + fun getUnusedId(): Long { + var id = 1L + val idsSum = idsSum + while (id and idsSum != 0L) { + id = id.shl(1) + } + return id + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt new file mode 100755 index 000000000..044644a91 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt @@ -0,0 +1,372 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.utils.cnCompare +import io.legado.app.utils.splitNotBlank +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@Dao +interface BookSourceDao { + + @Query("select * from book_sources_part order by customOrder asc") + fun flowAll(): Flow> + + @Query( + """select bp.* + from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl + where b.bookSourceName like '%' || :searchKey || '%' + or b.bookSourceGroup like '%' || :searchKey || '%' + or b.bookSourceUrl like '%' || :searchKey || '%' + or b.bookSourceComment like '%' || :searchKey || '%' + order by b.customOrder asc""" + ) + fun flowSearch(searchKey: String): Flow> + + @Query( + """select * from book_sources + where bookSourceName like '%' || :searchKey || '%' + or bookSourceGroup like '%' || :searchKey || '%' + or bookSourceUrl like '%' || :searchKey || '%' + or bookSourceComment like '%' || :searchKey || '%' + order by customOrder asc""" + ) + fun search(searchKey: String): List + + @Query( + """select bp.* + from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl + where b.enabled = 1 + and (b.bookSourceName like '%' || :searchKey || '%' + or b.bookSourceGroup like '%' || :searchKey || '%' + or b.bookSourceUrl like '%' || :searchKey || '%' + or b.bookSourceComment like '%' || :searchKey || '%') + order by b.customOrder asc""" + ) + fun flowSearchEnabled(searchKey: String): Flow> + + @Query( + """select * from book_sources_part + where bookSourceGroup = :searchKey + or bookSourceGroup like :searchKey || ',%' + or bookSourceGroup like '%,' || :searchKey + or bookSourceGroup like '%,' || :searchKey || ',%' + order by customOrder asc""" + ) + fun flowGroupSearch(searchKey: String): Flow> + + @Query( + """select * from book_sources + where bookSourceGroup = :searchKey + or bookSourceGroup like :searchKey || ',%' + or bookSourceGroup like '%,' || :searchKey + or bookSourceGroup like '%,' || :searchKey || ',%' + order by customOrder asc""" + ) + fun groupSearch(searchKey: String): List + + @Query("select * from book_sources_part where enabled = 1 order by customOrder asc") + fun flowEnabled(): Flow> + + @Query("select * from book_sources_part where enabled = 0 order by customOrder asc") + fun flowDisabled(): Flow> + + @Query( + """select * from book_sources_part + where enabledExplore = 1 and hasExploreUrl = 1 order by customOrder asc""" + ) + fun flowExplore(): Flow> + + @Query("select * from book_sources_part where hasLoginUrl = 1 order by customOrder asc") + fun flowLogin(): Flow> + + @Query( + """select * from book_sources_part + where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%' + order by customOrder asc""" + ) + fun flowNoGroup(): Flow> + + @Query("select * from book_sources_part where enabledExplore = 1 order by customOrder asc") + fun flowEnabledExplore(): Flow> + + @Query("select * from book_sources_part where enabledExplore = 0 order by customOrder asc") + fun flowDisabledExplore(): Flow> + + @Query( + """select * from book_sources_part + where enabledExplore = 1 + and hasExploreUrl = 1 + and (bookSourceGroup like '%' || :key || '%' + or bookSourceName like '%' || :key || '%') + order by customOrder asc""" + ) + fun flowExplore(key: String): Flow> + + @Query( + """select * from book_sources_part + where enabledExplore = 1 + and hasExploreUrl = 1 + and (bookSourceGroup = :key + or bookSourceGroup like :key || ',%' + or bookSourceGroup like '%,' || :key + or bookSourceGroup like '%,' || :key || ',%') + order by customOrder asc""" + ) + fun flowGroupExplore(key: String): Flow> + + @Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") + fun flowGroupsUnProcessed(): Flow> + + @Query( + """select distinct bookSourceGroup from book_sources + where enabled = 1 and trim(bookSourceGroup) <> ''""" + ) + fun flowEnabledGroupsUnProcessed(): Flow> + + @Query( + """select distinct bookSourceGroup from book_sources + where enabledExplore = 1 + and trim(exploreUrl) <> '' + and trim(bookSourceGroup) <> '' + order by customOrder""" + ) + fun flowExploreGroupsUnProcessed(): Flow> + + @Query( + """select * from book_sources + where bookSourceGroup like '%' || :group || '%' order by customOrder asc""" + ) + fun getByGroup(group: String): List + + @Query( + """select * from book_sources + where enabled = 1 + and (bookSourceGroup = :group + or bookSourceGroup like :group || ',%' + or bookSourceGroup like '%,' || :group + or bookSourceGroup like '%,' || :group || ',%') + order by customOrder asc""" + ) + fun getEnabledByGroup(group: String): List + + @Query( + """select * from book_sources_part + where enabled = 1 + and (bookSourceGroup = :group + or bookSourceGroup like :group || ',%' + or bookSourceGroup like '%,' || :group + or bookSourceGroup like '%,' || :group || ',%') + order by customOrder asc""" + ) + fun getEnabledPartByGroup(group: String): List + + @Query( + """select * from book_sources + where bookUrlPattern != 'NONE' and bookSourceType = :type order by customOrder asc""" + ) + fun getEnabledByType(type: Int): List + + @Query("select * from book_sources where enabled = 1 and bookSourceUrl = :baseUrl") + fun getBookSourceAddBook(baseUrl: String): BookSource? + + @get:Query( + """select bp.* + from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl + where b.enabled = 1 + and trim(b.bookUrlPattern) <> '' + and trim(b.bookUrlPattern) <> 'NONE' + order by b.customOrder""" + ) + val hasBookUrlPattern: List + + @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''") + val noGroup: List + + @get:Query("select * from book_sources order by customOrder asc") + val all: List + + @get:Query("select * from book_sources_part order by customOrder asc") + val allPart: List + + @get:Query("select * from book_sources where enabled = 1 order by customOrder") + val allEnabled: List + + @get:Query("select * from book_sources_part where enabled = 1 order by customOrder asc") + val allEnabledPart: List + + @get:Query("select * from book_sources where enabled = 0 order by customOrder") + val allDisabled: List + + @get:Query( + """select * from book_sources + where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'""" + ) + val allNoGroup: List + + @get:Query("select * from book_sources where enabledExplore = 1 order by customOrder") + val allEnabledExplore: List + + @get:Query("select * from book_sources where enabledExplore = 0 order by customOrder") + val allDisabledExplore: List + + @get:Query("select * from book_sources where loginUrl is not null and loginUrl != ''") + val allLogin: List + + @get:Query( + """select bp.* + from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl + where b.enabled = 1 and b.bookSourceType = 0 order by b.customOrder""" + ) + val allTextEnabledPart: List + + @get:Query( + """select distinct bookSourceGroup from book_sources + where trim(bookSourceGroup) <> ''""" + ) + val allGroupsUnProcessed: List + + @get:Query( + """select distinct bookSourceGroup from book_sources + where enabled = 1 and trim(bookSourceGroup) <> ''""" + ) + val allEnabledGroupsUnProcessed: List + + @Query("select * from book_sources where bookSourceUrl = :key") + fun getBookSource(key: String): BookSource? + + @Query("select * from book_sources_part where bookSourceUrl = :key") + fun getBookSourcePart(key: String): BookSourcePart? + + @Query("select count(*) from book_sources") + fun allCount(): Int + + @Query("SELECT EXISTS(select 1 from book_sources where bookSourceUrl = :key)") + fun has(key: String): Boolean + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg bookSource: BookSource) + + @Update + fun update(vararg bookSource: BookSource) + + @Delete + fun delete(vararg bookSource: BookSource) + + @Query("delete from book_sources where bookSourceUrl = :key") + fun delete(key: String) + + @Transaction + fun delete(bookSources: List) { + for (bs in bookSources) { + delete(bs.bookSourceUrl) + } + } + + @get:Query("select min(customOrder) from book_sources") + val minOrder: Int + + @get:Query("select max(customOrder) from book_sources") + val maxOrder: Int + + @get:Query( + """select exists (select 1 + from book_sources group by customOrder having count(customOrder) > 1)""" + ) + val hasDuplicateOrder: Boolean + + @Query("update book_sources set enabled = :enable where bookSourceUrl = :bookSourceUrl") + fun enable(bookSourceUrl: String, enable: Boolean) + + @Transaction + fun enable(enable: Boolean, bookSources: List) { + for (bs in bookSources) { + enable(bs.bookSourceUrl, enable) + } + } + + @Query("update book_sources set enabledExplore = :enable where bookSourceUrl = :bookSourceUrl") + fun enableExplore(bookSourceUrl: String, enable: Boolean) + + @Transaction + fun enableExplore(enable: Boolean, bookSources: List) { + for (bs in bookSources) { + enableExplore(bs.bookSourceUrl, enable) + } + } + + @Query( + """update book_sources + set customOrder = :customOrder where bookSourceUrl = :bookSourceUrl""" + ) + fun upOrder(bookSourceUrl: String, customOrder: Int) + + @Transaction + fun upOrder(bookSources: List) { + for (bs in bookSources) { + upOrder(bs.bookSourceUrl, bs.customOrder) + } + } + + fun upOrder(bookSource: BookSourcePart) { + upOrder(bookSource.bookSourceUrl, bookSource.customOrder) + } + + @Query( + """update book_sources + set bookSourceGroup = :bookSourceGroup where bookSourceUrl = :bookSourceUrl""" + ) + fun upGroup(bookSourceUrl: String, bookSourceGroup: String) + + @Transaction + fun upGroup(bookSources: List) { + for (bs in bookSources) { + bs.bookSourceGroup?.let { upGroup(bs.bookSourceUrl, it) } + } + } + + private fun dealGroups(list: List): List { + val groups = linkedSetOf() + list.forEach { + it.splitNotBlank(AppPattern.splitGroupRegex).forEach { group -> + groups.add(group) + } + } + return groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + } + } + + fun allGroups(): List = dealGroups(allGroupsUnProcessed) + + fun allEnabledGroups(): List = dealGroups(allEnabledGroupsUnProcessed) + + fun flowGroups(): Flow> { + return flowGroupsUnProcessed().map { list -> + dealGroups(list) + }.flowOn(IO) + } + + fun flowExploreGroups(): Flow> { + return flowExploreGroupsUnProcessed().map { list -> + dealGroups(list) + }.flowOn(IO) + } + + fun flowEnabledGroups(): Flow> { + return flowEnabledGroupsUnProcessed().map { list -> + dealGroups(list) + }.flowOn(IO) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt b/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt new file mode 100755 index 000000000..7940139ff --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/BookmarkDao.kt @@ -0,0 +1,60 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.Bookmark +import kotlinx.coroutines.flow.Flow + + +@Dao +interface BookmarkDao { + + @get:Query( + """ + select * from bookmarks order by bookName collate localized, bookAuthor collate localized, chapterIndex, chapterPos + """ + ) + val all: List + + @Query("select * from bookmarks order by bookName collate localized, bookAuthor collate localized, chapterIndex, chapterPos") + fun flowAll(): Flow> + + @Query( + """select * from bookmarks + where bookName = :bookName and bookAuthor = :bookAuthor + order by chapterIndex""" + ) + fun flowByBook(bookName: String, bookAuthor: String): Flow> + + @Query( + """SELECT * FROM bookmarks + where bookName = :bookName and bookAuthor = :bookAuthor + and chapterName like '%'||:key||'%' or content like '%'||:key||'%' + order by chapterIndex""" + ) + fun flowSearch(bookName: String, bookAuthor: String, key: String): Flow> + + @Query( + """select * from bookmarks + where bookName = :bookName and bookAuthor = :bookAuthor + order by chapterIndex""" + ) + fun getByBook(bookName: String, bookAuthor: String): List + + @Query( + """SELECT * FROM bookmarks + where bookName = :bookName and bookAuthor = :bookAuthor + and chapterName like '%'||:key||'%' or content like '%'||:key||'%' + order by chapterIndex""" + ) + fun search(bookName: String, bookAuthor: String, key: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg bookmark: Bookmark) + + @Update + fun update(bookmark: Bookmark) + + @Delete + fun delete(vararg bookmark: Bookmark) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/CacheDao.kt b/app/src/main/java/io/legado/app/data/dao/CacheDao.kt new file mode 100755 index 000000000..c3cdb331e --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/CacheDao.kt @@ -0,0 +1,35 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.legado.app.data.entities.Cache + +@Dao +interface CacheDao { + + @Query("select * from caches where `key` = :key") + fun get(key: String): Cache? + + @Query("select value from caches where `key` = :key and (deadline = 0 or deadline > :now)") + fun get(key: String, now: Long): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg cache: Cache) + + @Query("delete from caches where `key` = :key") + fun delete(key: String) + + @Query( + """delete from caches where `key` like 'v_' || :key || '_%' + or `key` = 'userInfo_' || :key + or `key` = 'loginHeader_' || :key + or `key` = 'sourceVariable_' || :key""" + ) + fun deleteSourceVariables(key: String) + + @Query("delete from caches where deadline > 0 and deadline < :now") + fun clearDeadline(now: Long) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/CookieDao.kt b/app/src/main/java/io/legado/app/data/dao/CookieDao.kt new file mode 100755 index 000000000..f667b7d2d --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/CookieDao.kt @@ -0,0 +1,26 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.Cookie + +@Dao +interface CookieDao { + + @Query("SELECT * FROM cookies Where url = :url") + fun get(url: String): Cookie? + + @Query("select * from cookies where url like '%|%'") + fun getOkHttpCookies(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg cookie: Cookie) + + @Update + fun update(vararg cookie: Cookie) + + @Query("delete from cookies where url = :url") + fun delete(url: String) + + @Query("delete from cookies where url like '%|%'") + fun deleteOkHttp() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/DictRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/DictRuleDao.kt new file mode 100755 index 000000000..6dfd601bf --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/DictRuleDao.kt @@ -0,0 +1,32 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.DictRule +import kotlinx.coroutines.flow.Flow + + +@Dao +interface DictRuleDao { + + @get:Query("select * from dictRules order by sortNumber") + val all: List + + @get:Query("select * from dictRules where enabled = 1 order by sortNumber") + val enabled: List + + @Query("select * from dictRules order by sortNumber") + fun flowAll(): Flow> + + @Query("select * from dictRules where name = :name") + fun getByName(name: String): DictRule? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg dictRule: DictRule) + + @Update + fun update(vararg dictRule: DictRule) + + @Delete + fun delete(vararg dictRule: DictRule) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt b/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt new file mode 100755 index 000000000..b75fd290a --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt @@ -0,0 +1,36 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.HttpTTS +import kotlinx.coroutines.flow.Flow + +@Dao +interface HttpTTSDao { + + @get:Query("select * from httpTTS order by name") + val all: List + + @Query("select * from httpTTS order by name") + fun flowAll(): Flow> + + @get:Query("select count(*) from httpTTS") + val count: Int + + @Query("select * from httpTTS where id = :id") + fun get(id: Long): HttpTTS? + + @Query("select name from httpTTS where id = :id") + fun getName(id: Long): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg httpTTS: HttpTTS) + + @Delete + fun delete(vararg httpTTS: HttpTTS) + + @Update + fun update(vararg httpTTS: HttpTTS) + + @Query("delete from httpTTS where id < 0") + fun deleteDefault() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/KeyboardAssistsDao.kt b/app/src/main/java/io/legado/app/data/dao/KeyboardAssistsDao.kt new file mode 100755 index 000000000..b25fec1a9 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/KeyboardAssistsDao.kt @@ -0,0 +1,34 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.KeyboardAssist +import kotlinx.coroutines.flow.Flow + +@Dao +interface KeyboardAssistsDao { + + @get:Query("select * from keyboardAssists order by serialNo") + val all: List + + @Query("select * from keyboardAssists where type = :type order by serialNo") + fun getByType(type: Int): List + + @get:Query("select * from keyboardAssists order by serialNo") + val flowAll: Flow> + + @Query("select * from keyboardAssists where type = :type order by serialNo") + fun flowByType(type: Int): Flow> + + @get:Query("select max(serialNo) from keyboardAssists order by serialNo") + val maxSerialNo: Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg keyboardAssist: KeyboardAssist) + + @Update + fun update(vararg keyboardAssist: KeyboardAssist) + + @Delete + fun delete(vararg keyboardAssist: KeyboardAssist) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt new file mode 100755 index 000000000..2846fe844 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt @@ -0,0 +1,55 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.ReadRecord +import io.legado.app.data.entities.ReadRecordShow + +@Dao +interface ReadRecordDao { + + @get:Query("select * from readRecord") + val all: List + + @get:Query( + """ + select bookName, sum(readTime) as readTime, max(lastRead) as lastRead + from readRecord + group by bookName + order by bookName collate localized""" + ) + val allShow: List + + @get:Query("select sum(readTime) from readRecord") + val allTime: Long + + @Query( + """ + select bookName, sum(readTime) as readTime, max(lastRead) as lastRead + from readRecord + where bookName like '%' || :searchKey || '%' + group by bookName + order by bookName collate localized""" + ) + fun search(searchKey: String): List + + @Query("select sum(readTime) from readRecord where bookName = :bookName") + fun getReadTime(bookName: String): Long? + + @Query("select readTime from readRecord where deviceId = :androidId and bookName = :bookName") + fun getReadTime(androidId: String, bookName: String): Long? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg readRecord: ReadRecord) + + @Update + fun update(vararg record: ReadRecord) + + @Delete + fun delete(vararg record: ReadRecord) + + @Query("delete from readRecord") + fun clear() + + @Query("delete from readRecord where bookName = :bookName") + fun deleteByName(bookName: String) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt new file mode 100755 index 000000000..09867c9b9 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt @@ -0,0 +1,114 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.utils.cnCompare +import io.legado.app.utils.splitNotBlank +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + + +@Dao +interface ReplaceRuleDao { + + @Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC") + fun flowAll(): Flow> + + @Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC") + fun flowSearch(key: String): Flow> + + @Query("SELECT * FROM replace_rules where `group` like :key ORDER BY sortOrder ASC") + fun flowGroupSearch(key: String): Flow> + + @Query("select `group` from replace_rules where `group` is not null and `group` <> ''") + fun flowGroupsUnProcessed(): Flow> + + @Query("select * from replace_rules where `group` is null or trim(`group`) = '' or trim(`group`) like '%未分组%'") + fun flowNoGroup(): Flow> + + @get:Query("SELECT MIN(sortOrder) FROM replace_rules") + val minOrder: Int + + @get:Query("SELECT MAX(sortOrder) FROM replace_rules") + val maxOrder: Int + + @get:Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC") + val all: List + + @get:Query("select distinct `group` from replace_rules where trim(`group`) <> ''") + val allGroupsUnProcessed: List + + @get:Query("SELECT * FROM replace_rules WHERE isEnabled = 1 ORDER BY sortOrder ASC") + val allEnabled: List + + @Query("SELECT * FROM replace_rules WHERE id = :id") + fun findById(id: Long): ReplaceRule? + + @Query("SELECT * FROM replace_rules WHERE id in (:ids)") + fun findByIds(vararg ids: Long): List + + @Query( + """SELECT * FROM replace_rules WHERE isEnabled = 1 and scopeContent = 1 + AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '') + and (excludeScope is null or (excludeScope not LIKE '%' || :name || '%' and excludeScope not LIKE '%' || :origin || '%')) + order by sortOrder""" + ) + fun findEnabledByContentScope(name: String, origin: String): List + + @Query( + """SELECT * FROM replace_rules WHERE isEnabled = 1 and scopeTitle = 1 + AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '') + and (excludeScope is null or (excludeScope not LIKE '%' || :name || '%' and excludeScope not LIKE '%' || :origin || '%')) + order by sortOrder""" + ) + fun findEnabledByTitleScope(name: String, origin: String): List + + @Query("select * from replace_rules where `group` like '%' || :group || '%'") + fun getByGroup(group: String): List + + @get:Query("select * from replace_rules where `group` is null or `group` = ''") + val noGroup: List + + @get:Query("SELECT COUNT(*) - SUM(isEnabled) FROM replace_rules") + val summary: Int + + @Query("UPDATE replace_rules SET isEnabled = :enable") + fun enableAll(enable: Boolean) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg replaceRule: ReplaceRule): List + + @Update + fun update(vararg replaceRules: ReplaceRule) + + @Delete + fun delete(vararg replaceRules: ReplaceRule) + + private fun dealGroups(list: List): List { + val groups = linkedSetOf() + list.forEach { + it.splitNotBlank(AppPattern.splitGroupRegex).forEach { group -> + groups.add(group) + } + } + return groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + } + } + + fun allGroups(): List = dealGroups(allGroupsUnProcessed) + + fun flowGroups(): Flow> { + return flowGroupsUnProcessed().map { list -> + dealGroups(list) + }.flowOn(IO) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt new file mode 100755 index 000000000..c8742f8fc --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt @@ -0,0 +1,40 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.RssArticle +import kotlinx.coroutines.flow.Flow + +@Dao +interface RssArticleDao { + + @Query("select * from rssArticles where origin = :origin and link = :link") + fun get(origin: String, link: String): RssArticle? + + @Query( + """select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content, + t1.description, t1.image, t1.`group`, t1.pubDate, t1.variable, ifNull(t2.read, 0) as read + from rssArticles as t1 left join rssReadRecords as t2 + on t1.link = t2.record where origin = :origin and sort = :sort + order by `order` desc""" + ) + fun flowByOriginSort(origin: String, sort: String): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg rssArticle: RssArticle) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun append(vararg rssArticle: RssArticle) + + @Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order") + fun clearOld(origin: String, sort: String, order: Long) + + @Update + fun update(vararg rssArticle: RssArticle) + + @Query("update rssArticles set origin = :origin where origin = :oldOrigin") + fun updateOrigin(origin: String, oldOrigin: String) + + @Query("delete from rssArticles where origin = :origin") + fun delete(origin: String) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/RssReadRecordDao.kt b/app/src/main/java/io/legado/app/data/dao/RssReadRecordDao.kt new file mode 100755 index 000000000..138a72a4e --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RssReadRecordDao.kt @@ -0,0 +1,21 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.RssReadRecord + +@Dao +interface RssReadRecordDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertRecord(vararg rssReadRecord: RssReadRecord) + + @Query("select * from rssReadRecords order by readTime desc") + fun getRecords(): List + + @get:Query("select count(1) from rssReadRecords") + val countRecords: Int + + @Query("delete from rssReadRecords") + fun deleteAllRecord() + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt new file mode 100755 index 000000000..489fce0dd --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt @@ -0,0 +1,157 @@ +package io.legado.app.data.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.RssSource +import io.legado.app.utils.cnCompare +import io.legado.app.utils.splitNotBlank +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@Dao +interface RssSourceDao { + + @Query("select * from rssSources where sourceUrl = :key") + fun getByKey(key: String): RssSource? + + @Query("select * from rssSources where sourceUrl in (:sourceUrls)") + fun getRssSources(vararg sourceUrls: String): List + + @get:Query("SELECT * FROM rssSources order by customOrder") + val all: List + + @get:Query("select count(sourceUrl) from rssSources") + val size: Int + + @Query("SELECT * FROM rssSources order by customOrder") + fun flowAll(): Flow> + + @Query( + """SELECT * FROM rssSources + where sourceName like '%' || :key || '%' + or sourceUrl like '%' || :key || '%' + or sourceGroup like '%' || :key || '%' + or sourceComment like '%' || :key || '%' + order by customOrder""" + ) + fun flowSearch(key: String): Flow> + + @Query( + """SELECT * FROM rssSources + where (sourceGroup = :key + or sourceGroup like :key || ',%' + or sourceGroup like '%,' || :key + or sourceGroup like '%,' || :key || ',%') + order by customOrder""" + ) + fun flowGroupSearch(key: String): Flow> + + @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder") + fun flowEnabled(): Flow> + + @Query("SELECT * FROM rssSources where enabled = 0 order by customOrder") + fun flowDisabled(): Flow> + + @Query("select * from rssSources where loginUrl is not null and loginUrl != ''") + fun flowLogin(): Flow> + + @Query("select * from rssSources where sourceGroup is null or sourceGroup = '' or sourceGroup like '%未分组%'") + fun flowNoGroup(): Flow> + + @Query( + """SELECT * FROM rssSources + where enabled = 1 + and (sourceName like '%' || :searchKey || '%' + or sourceGroup like '%' || :searchKey || '%' + or sourceUrl like '%' || :searchKey || '%' + or sourceComment like '%' || :searchKey || '%') + order by customOrder""" + ) + fun flowEnabled(searchKey: String): Flow> + + @Query( + """SELECT * FROM rssSources + where enabled = 1 and (sourceGroup = :searchKey + or sourceGroup like :searchKey || ',%' + or sourceGroup like '%,' || :searchKey + or sourceGroup like '%,' || :searchKey || ',%') + order by customOrder""" + ) + fun flowEnabledByGroup(searchKey: String): Flow> + + @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") + fun flowGroupsUnProcessed(): Flow> + + @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> '' and enabled = 1") + fun flowEnabledGroupsUnProcessed(): Flow> + + @get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") + val allGroupsUnProcessed: List + + @get:Query("select min(customOrder) from rssSources") + val minOrder: Int + + @get:Query("select max(customOrder) from rssSources") + val maxOrder: Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg rssSource: RssSource) + + @Update + fun update(vararg rssSource: RssSource) + + @Delete + fun delete(vararg rssSource: RssSource) + + @Query("delete from rssSources where sourceUrl = :sourceUrl") + fun delete(sourceUrl: String) + + @Query("delete from rssSources where sourceGroup like 'legado'") + fun deleteDefault() + + @get:Query("select * from rssSources where sourceGroup is null or sourceGroup = ''") + val noGroup: List + + @Query("select * from rssSources where sourceGroup like '%' || :group || '%'") + fun getByGroup(group: String): List + + @Query("select exists(select 1 from rssSources where sourceUrl = :key)") + fun has(key: String): Boolean + + @Query("update rssSources set enabled = :enable where sourceUrl = :sourceUrl") + fun enable(sourceUrl: String, enable: Boolean) + + private fun dealGroups(list: List): List { + val groups = linkedSetOf() + list.forEach { + it.splitNotBlank(AppPattern.splitGroupRegex).forEach { group -> + groups.add(group) + } + } + return groups.sortedWith { o1, o2 -> + o1.cnCompare(o2) + } + } + + fun allGroups(): List = dealGroups(allGroupsUnProcessed) + + fun flowGroups(): Flow> { + return flowGroupsUnProcessed().map { list -> + dealGroups(list) + }.flowOn(IO) + } + + fun flowEnabledGroups(): Flow> { + return flowEnabledGroupsUnProcessed().map { list -> + dealGroups(list) + }.flowOn(IO) + } + +} diff --git a/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt b/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt new file mode 100755 index 000000000..083b8f6bf --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt @@ -0,0 +1,45 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.RssStar +import kotlinx.coroutines.flow.Flow + +@Dao +interface RssStarDao { + + @get:Query("select * from rssStars order by starTime desc") + val all: List + + @Query("select `group` from rssStars group by `group` order by `group`") + fun flowGroups(): Flow> + + @Query("select * from rssStars where `group` = :group order by starTime desc") + fun flowByGroup(group: String): Flow> + + @Query("select * from rssStars where origin = :origin and link = :link") + fun get(origin: String, link: String): RssStar? + + @Query("select * from rssStars order by starTime desc") + fun liveAll(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg rssStar: RssStar) + + @Update + fun update(vararg rssStar: RssStar) + + @Query("update rssStars set origin = :origin where origin = :oldOrigin") + fun updateOrigin(origin: String, oldOrigin: String) + + @Query("delete from rssStars where origin = :origin") + fun delete(origin: String) + + @Query("delete from rssStars where origin = :origin and link = :link") + fun delete(origin: String, link: String) + + @Query("delete from rssStars where `group` = :group") + fun deleteByGroup(group: String) + + @Query("delete from rssStars") + fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt b/app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt new file mode 100755 index 000000000..56d87b262 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt @@ -0,0 +1,30 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.RuleSub +import kotlinx.coroutines.flow.Flow + +@Dao +interface RuleSubDao { + + @get:Query("select * from ruleSubs order by customOrder") + val all: List + + @Query("select * from ruleSubs order by customOrder") + fun flowAll(): Flow> + + @get:Query("select customOrder from ruleSubs order by customOrder limit 0,1") + val maxOrder: Int + + @Query("select * from ruleSubs where url = :url") + fun findByUrl(url: String): RuleSub? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg ruleSub: RuleSub) + + @Delete + fun delete(vararg ruleSub: RuleSub) + + @Update + fun update(vararg ruleSub: RuleSub) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt new file mode 100755 index 000000000..9444a0964 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt @@ -0,0 +1,73 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.SearchBook + +@Dao +interface SearchBookDao { + + @Query("select * from searchBooks where bookUrl = :bookUrl") + fun getSearchBook(bookUrl: String): SearchBook? + + @Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources) order by originOrder limit 1") + fun getFirstByNameAuthor(name: String, author: String): SearchBook? + + @Query( + """select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, + t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, + t1.wordCount, t2.customOrder as originOrder, t1.chapterWordCountText, t1.respondTime, t1.chapterWordCount + from searchBooks as t1 inner join book_sources as t2 + on t1.origin = t2.bookSourceUrl + where t1.name = :name and t1.author like '%'||:author||'%' + and t2.enabled = 1 and t2.bookSourceGroup like '%'||:sourceGroup||'%' + order by t2.customOrder""" + ) + fun changeSourceByGroup(name: String, author: String, sourceGroup: String): List + + @Query( + """select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, + t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, + t1.wordCount, t2.customOrder as originOrder, t1.chapterWordCountText, t1.respondTime, t1.chapterWordCount + from searchBooks as t1 inner join book_sources as t2 + on t1.origin = t2.bookSourceUrl + where t1.name = :name and t1.author like '%'||:author||'%' + and t2.bookSourceGroup like '%'||:sourceGroup||'%' + and (originName like '%'||:key||'%' or t1.latestChapterTitle like '%'||:key||'%') + and t2.enabled = 1 + order by t2.customOrder""" + ) + fun changeSourceSearch( + name: String, + author: String, + key: String, + sourceGroup: String + ): List + + @Query( + """ + select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, + t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, + t1.wordCount, t2.customOrder as originOrder, t1.chapterWordCountText, t1.respondTime, t1.chapterWordCount + from searchBooks as t1 inner join book_sources as t2 + on t1.origin = t2.bookSourceUrl + where t1.name = :name and t1.author = :author and t1.coverUrl is not null and t1.coverUrl <> '' and t2.enabled = 1 + order by t2.customOrder + """ + ) + fun getEnableHasCover(name: String, author: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg searchBook: SearchBook): List + + @Query("delete from searchBooks where name = :name and author = :author") + fun clear(name: String, author: String) + + @Query("delete from searchBooks where time < :time") + fun clearExpired(time: Long) + + @Update + fun update(vararg searchBook: SearchBook) + + @Delete + fun delete(vararg searchBook: SearchBook) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt new file mode 100755 index 000000000..2b433e488 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/SearchKeywordDao.kt @@ -0,0 +1,38 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.SearchKeyword +import kotlinx.coroutines.flow.Flow + + +@Dao +interface SearchKeywordDao { + + @get:Query("SELECT * FROM search_keywords") + val all: List + + @Query("SELECT * FROM search_keywords ORDER BY usage DESC") + fun flowByUsage(): Flow> + + @Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC") + fun flowByTime(): Flow> + + @Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC") + fun flowSearch(key: String): Flow> + + @Query("select * from search_keywords where word = :key") + fun get(key: String): SearchKeyword? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg keywords: SearchKeyword) + + @Update + fun update(vararg keywords: SearchKeyword) + + @Delete + fun delete(vararg keywords: SearchKeyword) + + @Query("DELETE FROM search_keywords") + fun deleteAll() + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/ServerDao.kt b/app/src/main/java/io/legado/app/data/dao/ServerDao.kt new file mode 100755 index 000000000..fd6b41198 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/ServerDao.kt @@ -0,0 +1,33 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.Server +import kotlinx.coroutines.flow.Flow + +@Dao +interface ServerDao { + + @Query("select * from servers order by sortNumber") + fun observeAll(): Flow> + + @get:Query("select * from servers order by sortNumber") + val all: List + + @Query("select * from servers where id = :id") + fun get(id: Long): Server? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg server: Server) + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(vararg server: Server) + + @Delete + fun delete(vararg server: Server) + + @Query("delete from servers where id = :id") + fun delete(id: Long) + + @Query("delete from servers where id < 0") + fun deleteDefault() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt new file mode 100755 index 000000000..32981d1d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt @@ -0,0 +1,45 @@ +package io.legado.app.data.dao + +import androidx.room.* +import io.legado.app.data.entities.TxtTocRule +import kotlinx.coroutines.flow.Flow + +@Dao +interface TxtTocRuleDao { + + @Query("select * from txtTocRules order by serialNumber") + fun observeAll(): Flow> + + @get:Query("select * from txtTocRules order by serialNumber") + val all: List + + @get:Query("select * from txtTocRules where enable = 1 order by serialNumber") + val enabled: List + + @get:Query("select * from txtTocRules where enable != 1 order by serialNumber") + val disabled: List + + @get:Query("select count(*) from txtTocRules") + val count: Int + + @Query("select * from txtTocRules where id = :id") + fun get(id: Long): TxtTocRule? + + @get:Query("select ifNull(min(serialNumber), 0) from txtTocRules") + val minOrder: Int + + @get:Query("select ifNull(max(serialNumber), 0) from txtTocRules") + val maxOrder: Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg rule: TxtTocRule) + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(vararg rule: TxtTocRule) + + @Delete + fun delete(vararg rule: TxtTocRule) + + @Query("delete from txtTocRules where id < 0") + fun deleteDefault() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BaseBook.kt b/app/src/main/java/io/legado/app/data/entities/BaseBook.kt new file mode 100755 index 000000000..a469dcd99 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BaseBook.kt @@ -0,0 +1,53 @@ +package io.legado.app.data.entities + +import io.legado.app.help.RuleBigDataHelp +import io.legado.app.model.analyzeRule.RuleDataInterface +import io.legado.app.utils.GSON +import io.legado.app.utils.splitNotBlank + +interface BaseBook : RuleDataInterface { + var name: String + var author: String + var bookUrl: String + var kind: String? + var wordCount: String? + var variable: String? + + var infoHtml: String? + var tocHtml: String? + + override fun putVariable(key: String, value: String?): Boolean { + if (super.putVariable(key, value)) { + variable = GSON.toJson(variableMap) + } + return true + } + + fun putCustomVariable(value: String?) { + putVariable("custom", value) + } + + fun getCustomVariable(): String { + return getVariable("custom") + } + + override fun putBigVariable(key: String, value: String?) { + RuleBigDataHelp.putBookVariable(bookUrl, key, value) + } + + override fun getBigVariable(key: String): String? { + return RuleBigDataHelp.getBookVariable(bookUrl, key) + } + + fun getKindList(): List { + val kindList = arrayListOf() + wordCount?.let { + if (it.isNotBlank()) kindList.add(it) + } + kind?.let { + val kinds = it.splitNotBlank(",", "\n") + kindList.addAll(kinds) + } + return kindList + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BaseRssArticle.kt b/app/src/main/java/io/legado/app/data/entities/BaseRssArticle.kt new file mode 100755 index 000000000..b42040a03 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BaseRssArticle.kt @@ -0,0 +1,29 @@ +package io.legado.app.data.entities + +import io.legado.app.help.RuleBigDataHelp +import io.legado.app.model.analyzeRule.RuleDataInterface +import io.legado.app.utils.GSON + +interface BaseRssArticle : RuleDataInterface { + + var origin: String + var link: String + + var variable: String? + + override fun putVariable(key: String, value: String?): Boolean { + if (super.putVariable(key, value)) { + variable = GSON.toJson(variableMap) + } + return true + } + + override fun putBigVariable(key: String, value: String?) { + RuleBigDataHelp.putRssVariable(origin, link, key, value) + } + + override fun getBigVariable(key: String): String? { + return RuleBigDataHelp.getRssVariable(origin, link, key) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BaseSource.kt b/app/src/main/java/io/legado/app/data/entities/BaseSource.kt new file mode 100755 index 000000000..8a67c9ef7 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BaseSource.kt @@ -0,0 +1,259 @@ +package io.legado.app.data.entities + +import cn.hutool.crypto.symmetric.AES +import com.script.ScriptBindings +import com.script.buildScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.rule.RowUi +import io.legado.app.help.CacheManager +import io.legado.app.help.JsExtensions +import io.legado.app.help.config.AppConfig +import io.legado.app.help.crypto.SymmetricCryptoAndroid +import io.legado.app.help.http.CookieStore +import io.legado.app.help.source.getShareScope +import io.legado.app.utils.GSON +import io.legado.app.utils.GSONStrict +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.has +import io.legado.app.utils.printOnDebug +import org.intellij.lang.annotations.Language + +/** + * 可在js里调用,source.xxx() + */ +@Suppress("unused") +interface BaseSource : JsExtensions { + /** + * 并发率 + */ + var concurrentRate: String? + + /** + * 登录地址 + */ + var loginUrl: String? + + /** + * 登录UI + */ + var loginUi: String? + + /** + * 请求头 + */ + var header: String? + + /** + * 启用cookieJar + */ + var enabledCookieJar: Boolean? + + /** + * js库 + */ + var jsLib: String? + + fun getTag(): String + + fun getKey(): String + + override fun getSource(): BaseSource? { + return this + } + + fun loginUi(): List? { + return GSON.fromJsonArray(loginUi).onFailure { + it.printOnDebug() + }.getOrNull() + } + + fun getLoginJs(): String? { + val loginJs = loginUrl + return when { + loginJs == null -> null + loginJs.startsWith("@js:") -> loginJs.substring(4) + loginJs.startsWith("") -> loginJs.substring(4, loginJs.lastIndexOf("<")) + else -> loginJs + } + } + + /** + * 调用login函数 实现登录请求 + */ + fun login() { + val loginJs = getLoginJs() + if (!loginJs.isNullOrBlank()) { + @Language("js") + val js = """$loginJs + if(typeof login=='function'){ + login.apply(this); + } else { + throw('Function login not implements!!!') + } + """.trimIndent() + evalJS(js) + } + } + + /** + * 解析header规则 + */ + fun getHeaderMap(hasLoginHeader: Boolean = false) = HashMap().apply { + header?.let { + try { + val json = when { + it.startsWith("@js:", true) -> evalJS(it.substring(4)).toString() + it.startsWith("", true) -> evalJS( + it.substring(4, it.lastIndexOf("<")) + ).toString() + + else -> it + } + GSONStrict.fromJsonObject>(json).getOrNull()?.let { map -> + putAll(map) + } ?: GSON.fromJsonObject>(json).getOrNull()?.let { map -> + log("请求头规则 JSON 格式不规范,请改为规范格式") + putAll(map) + } + } catch (e: Exception) { + AppLog.put("执行请求头规则出错\n$e", e) + } + } + if (!has(AppConst.UA_NAME, true)) { + put(AppConst.UA_NAME, AppConfig.userAgent) + } + if (hasLoginHeader) { + getLoginHeaderMap()?.let { + putAll(it) + } + } + } + + /** + * 获取用于登录的头部信息 + */ + fun getLoginHeader(): String? { + return CacheManager.get("loginHeader_${getKey()}") + } + + fun getLoginHeaderMap(): Map? { + val cache = getLoginHeader() ?: return null + return GSON.fromJsonObject>(cache).getOrNull() + } + + /** + * 保存登录头部信息,map格式,访问时自动添加 + */ + fun putLoginHeader(header: String) { + val headerMap = GSON.fromJsonObject>(header).getOrNull() + val cookie = headerMap?.get("Cookie") ?: headerMap?.get("cookie") + cookie?.let { + CookieStore.replaceCookie(getKey(), it) + } + CacheManager.put("loginHeader_${getKey()}", header) + } + + fun removeLoginHeader() { + CacheManager.delete("loginHeader_${getKey()}") + CookieStore.removeCookie(getKey()) + } + + /** + * 获取用户信息,可以用来登录 + * 用户信息采用aes加密存储 + */ + fun getLoginInfo(): String? { + try { + val key = AppConst.androidId.encodeToByteArray(0, 16) + val cache = CacheManager.get("userInfo_${getKey()}") ?: return null + return AES(key).decryptStr(cache) + } catch (e: Exception) { + AppLog.put("获取登陆信息出错", e) + return null + } + } + + fun getLoginInfoMap(): Map? { + return GSON.fromJsonObject>(getLoginInfo()).getOrNull() + } + + /** + * 保存用户信息,aes加密 + */ + fun putLoginInfo(info: String): Boolean { + return try { + val key = (AppConst.androidId).encodeToByteArray(0, 16) + val encodeStr = SymmetricCryptoAndroid("AES", key).encryptBase64(info) + CacheManager.put("userInfo_${getKey()}", encodeStr) + true + } catch (e: Exception) { + AppLog.put("保存登陆信息出错", e) + false + } + } + + fun removeLoginInfo() { + CacheManager.delete("userInfo_${getKey()}") + } + + /** + * 设置自定义变量 + * @param variable 变量内容 + */ + fun setVariable(variable: String?) { + if (variable != null) { + CacheManager.put("sourceVariable_${getKey()}", variable) + } else { + CacheManager.delete("sourceVariable_${getKey()}") + } + } + + /** + * 获取自定义变量 + */ + fun getVariable(): String { + return CacheManager.get("sourceVariable_${getKey()}") ?: "" + } + + /** + * 保存数据 + */ + fun put(key: String, value: String): String { + CacheManager.put("v_${getKey()}_${key}", value) + return value + } + + /** + * 获取保存的数据 + */ + fun get(key: String): String { + return CacheManager.get("v_${getKey()}_${key}") ?: "" + } + + /** + * 执行JS + */ + @Throws(Exception::class) + fun evalJS(jsStr: String, bindingsConfig: ScriptBindings.() -> Unit = {}): Any? { + val bindings = buildScriptBindings { bindings -> + bindings["java"] = this + bindings["source"] = this + bindings["baseUrl"] = getKey() + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + bindings.apply(bindingsConfig) + } + val sharedScope = getShareScope() + val scope = if (sharedScope == null) { + RhinoScriptEngine.getRuntimeScope(bindings) + } else { + bindings.apply { + prototype = sharedScope + } + } + return RhinoScriptEngine.eval(jsStr, scope) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt new file mode 100755 index 000000000..d0d411ff4 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -0,0 +1,416 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import io.legado.app.constant.AppPattern +import io.legado.app.constant.BookType +import io.legado.app.constant.PageAnim +import io.legado.app.data.appDb +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.getFolderNameNoCache +import io.legado.app.help.book.isEpub +import io.legado.app.help.book.isImage +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.model.ReadBook +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.nio.charset.Charset +import java.time.LocalDate +import kotlin.math.max + +@Parcelize +@TypeConverters(Book.Converters::class) +@Entity( + tableName = "books", + indices = [Index(value = ["name", "author"], unique = true)] +) +data class Book( + // 详情页Url(本地书源存储完整文件路径) + @PrimaryKey + @ColumnInfo(defaultValue = "") + override var bookUrl: String = "", + // 目录页Url (toc=table of Contents) + @ColumnInfo(defaultValue = "") + var tocUrl: String = "", + // 书源URL(默认BookType.local) + @ColumnInfo(defaultValue = BookType.localTag) + var origin: String = BookType.localTag, + //书源名称 or 本地书籍文件名 + @ColumnInfo(defaultValue = "") + var originName: String = "", + // 书籍名称(书源获取) + @ColumnInfo(defaultValue = "") + override var name: String = "", + // 作者名称(书源获取) + @ColumnInfo(defaultValue = "") + override var author: String = "", + // 分类信息(书源获取) + override var kind: String? = null, + // 分类信息(用户修改) + var customTag: String? = null, + // 封面Url(书源获取) + var coverUrl: String? = null, + // 封面Url(用户修改) + var customCoverUrl: String? = null, + // 简介内容(书源获取) + var intro: String? = null, + // 简介内容(用户修改) + var customIntro: String? = null, + // 自定义字符集名称(仅适用于本地书籍) + var charset: String? = null, + // 类型,详见BookType + @ColumnInfo(defaultValue = "0") + var type: Int = BookType.text, + // 自定义分组索引号 + @ColumnInfo(defaultValue = "0") + var group: Long = 0, + // 最新章节标题 + var latestChapterTitle: String? = null, + // 最新章节标题更新时间 + @ColumnInfo(defaultValue = "0") + var latestChapterTime: Long = System.currentTimeMillis(), + // 最近一次更新书籍信息的时间 + @ColumnInfo(defaultValue = "0") + var lastCheckTime: Long = System.currentTimeMillis(), + // 最近一次发现新章节的数量 + @ColumnInfo(defaultValue = "0") + var lastCheckCount: Int = 0, + // 书籍目录总数 + @ColumnInfo(defaultValue = "0") + var totalChapterNum: Int = 0, + // 当前章节名称 + var durChapterTitle: String? = null, + // 当前章节索引 + @ColumnInfo(defaultValue = "0") + var durChapterIndex: Int = 0, + // 当前阅读的进度(首行字符的索引位置) + @ColumnInfo(defaultValue = "0") + var durChapterPos: Int = 0, + // 最近一次阅读书籍的时间(打开正文的时间) + @ColumnInfo(defaultValue = "0") + var durChapterTime: Long = System.currentTimeMillis(), + //字数 + override var wordCount: String? = null, + // 刷新书架时更新书籍信息 + @ColumnInfo(defaultValue = "1") + var canUpdate: Boolean = true, + // 手动排序 + @ColumnInfo(defaultValue = "0") + var order: Int = 0, + //书源排序 + @ColumnInfo(defaultValue = "0") + var originOrder: Int = 0, + // 自定义书籍变量信息(用于书源规则检索书籍信息) + override var variable: String? = null, + //阅读设置 + var readConfig: ReadConfig? = null, + //同步时间 + @ColumnInfo(defaultValue = "0") + var syncTime: Long = 0L +) : Parcelable, BaseBook { + + override fun equals(other: Any?): Boolean { + if (other is Book) { + return other.bookUrl == bookUrl + } + return false + } + + override fun hashCode(): Int { + return bookUrl.hashCode() + } + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap: HashMap by lazy { + GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() + } + + @Ignore + @IgnoredOnParcel + override var infoHtml: String? = null + + @Ignore + @IgnoredOnParcel + override var tocHtml: String? = null + + @Ignore + @IgnoredOnParcel + var downloadUrls: List? = null + + @Ignore + @IgnoredOnParcel + private var folderName: String? = null + + @get:Ignore + @IgnoredOnParcel + val lastChapterIndex get() = totalChapterNum - 1 + + fun getRealAuthor() = author.replace(AppPattern.authorRegex, "") + + fun getUnreadChapterNum() = max(simulatedTotalChapterNum() - durChapterIndex - 1, 0) + + fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl + + fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro + + //自定义简介有自动更新的需求时,可通过更新intro再调用upCustomIntro()完成 + @Suppress("unused") + fun upCustomIntro() { + customIntro = intro + } + + fun fileCharset(): Charset { + return charset(charset ?: "UTF-8") + } + + @IgnoredOnParcel + val config: ReadConfig + get() { + if (readConfig == null) { + readConfig = ReadConfig() + } + return readConfig!! + } + + fun setReverseToc(reverseToc: Boolean) { + config.reverseToc = reverseToc + } + + fun getReverseToc(): Boolean { + return config.reverseToc + } + + fun setUseReplaceRule(useReplaceRule: Boolean) { + config.useReplaceRule = useReplaceRule + } + + fun getUseReplaceRule(): Boolean { + val useReplaceRule = config.useReplaceRule + if (useReplaceRule != null) { + return useReplaceRule + } + //图片类书源 epub本地 默认关闭净化 + if (isImage || isEpub) { + return false + } + return AppConfig.replaceEnableDefault + } + + fun setReSegment(reSegment: Boolean) { + config.reSegment = reSegment + } + + fun getReSegment(): Boolean { + return config.reSegment + } + + fun setPageAnim(pageAnim: Int?) { + config.pageAnim = pageAnim + } + + fun getPageAnim(): Int { + var pageAnim = config.pageAnim + ?: if (isImage) PageAnim.scrollPageAnim else ReadBookConfig.pageAnim + if (pageAnim < 0) { + pageAnim = ReadBookConfig.pageAnim + } + return pageAnim + } + + fun setImageStyle(imageStyle: String?) { + config.imageStyle = imageStyle + } + + fun getImageStyle(): String? { + return config.imageStyle + } + + fun setTtsEngine(ttsEngine: String?) { + config.ttsEngine = ttsEngine + } + + fun getTtsEngine(): String? { + return config.ttsEngine + } + + fun setSplitLongChapter(limitLongContent: Boolean) { + config.splitLongChapter = limitLongContent + } + + fun getSplitLongChapter(): Boolean { + return config.splitLongChapter + } + + // readSimulating 的 setter 和 getter + fun setReadSimulating(readSimulating: Boolean) { + config.readSimulating = readSimulating + } + + fun getReadSimulating(): Boolean { + return config.readSimulating + } + + // startDate 的 setter 和 getter + fun setStartDate(startDate: LocalDate?) { + config.startDate = startDate + } + + fun getStartDate(): LocalDate? { + if (!config.readSimulating || config.startDate == null) { + return LocalDate.now() + } + return config.startDate + } + + // startChapter 的 setter 和 getter + fun setStartChapter(startChapter: Int) { + config.startChapter = startChapter + } + + fun getStartChapter(): Int { + if (config.readSimulating) return config.startChapter ?: 0 + return this.durChapterIndex + } + + // dailyChapters 的 setter 和 getter + fun setDailyChapters(dailyChapters: Int) { + config.dailyChapters = dailyChapters + } + + fun getDailyChapters(): Int { + return config.dailyChapters + } + + fun getDelTag(tag: Long): Boolean { + return config.delTag and tag == tag + } + + fun addDelTag(tag: Long) { + config.delTag = config.delTag and tag + } + + fun removeDelTag(tag: Long) { + config.delTag = config.delTag and tag.inv() + } + + fun getFolderName(): String { + folderName?.let { + return it + } + //防止书名过长,只取9位 + folderName = getFolderNameNoCache() + return folderName!! + } + + fun toSearchBook() = SearchBook( + name = name, + author = author, + kind = kind, + bookUrl = bookUrl, + origin = origin, + originName = originName, + type = type, + wordCount = wordCount, + latestChapterTitle = latestChapterTitle, + coverUrl = coverUrl, + intro = intro, + tocUrl = tocUrl, + originOrder = originOrder, + variable = variable + ).apply { + this.infoHtml = this@Book.infoHtml + this.tocHtml = this@Book.tocHtml + } + + /** + * 迁移旧的书籍的一些信息到新的书籍中 + */ + fun migrateTo(newBook: Book, toc: List): Book { + newBook.durChapterIndex = BookHelp + .getDurChapter(durChapterIndex, durChapterTitle, toc, totalChapterNum) + newBook.durChapterTitle = toc[newBook.durChapterIndex].getDisplayTitle( + ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules(), + getUseReplaceRule() + ) + newBook.durChapterPos = durChapterPos + newBook.durChapterTime = durChapterTime + newBook.group = group + newBook.order = order + newBook.customCoverUrl = customCoverUrl + newBook.customIntro = customIntro + newBook.customTag = customTag + newBook.canUpdate = canUpdate + newBook.readConfig = readConfig + return newBook + } + + fun createBookMark(): Bookmark { + return Bookmark( + bookName = name, + bookAuthor = author, + ) + } + + fun save() { + if (appDb.bookDao.has(bookUrl)) { + appDb.bookDao.update(this) + } else { + appDb.bookDao.insert(this) + } + } + + fun delete() { + if (ReadBook.book?.bookUrl == bookUrl) { + ReadBook.book = null + } + appDb.bookDao.delete(this) + } + + @Suppress("ConstPropertyName") + companion object { + const val hTag = 2L + const val rubyTag = 4L + const val imgStyleDefault = "DEFAULT" + const val imgStyleFull = "FULL" + const val imgStyleText = "TEXT" + const val imgStyleSingle = "SINGLE" + } + + @Parcelize + data class ReadConfig( + var reverseToc: Boolean = false, + var pageAnim: Int? = null, + var reSegment: Boolean = false, + var imageStyle: String? = null, + var useReplaceRule: Boolean? = null,// 正文使用净化替换规则 + var delTag: Long = 0L,//去除标签 + var ttsEngine: String? = null, + var splitLongChapter: Boolean = true, + var readSimulating: Boolean = false, + var startDate: LocalDate? = null, + var startChapter: Int? = null, // 用户设置的起始章节 + var dailyChapters: Int = 3 // 用户设置的每日更新章节数 + ) : Parcelable + + class Converters { + + @TypeConverter + fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config) + + @TypeConverter + fun stringToReadConfig(json: String?) = GSON.fromJsonObject(json).getOrNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BookChapter.kt b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt new file mode 100755 index 000000000..558f03e84 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt @@ -0,0 +1,176 @@ +package io.legado.app.data.entities + +import android.annotation.SuppressLint +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.Index +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.exception.RegexTimeoutException +import io.legado.app.help.RuleBigDataHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.RuleDataInterface +import io.legado.app.utils.ChineseUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.replace +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CancellationException +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import splitties.init.appCtx + +@Parcelize +@Entity( + tableName = "chapters", + primaryKeys = ["url", "bookUrl"], + indices = [(Index(value = ["bookUrl"], unique = false)), + (Index(value = ["bookUrl", "index"], unique = true))], + foreignKeys = [(ForeignKey( + entity = Book::class, + parentColumns = ["bookUrl"], + childColumns = ["bookUrl"], + onDelete = ForeignKey.CASCADE + ))] +) // 删除书籍时自动删除章节 +data class BookChapter( + var url: String = "", // 章节地址 + var title: String = "", // 章节标题 + var isVolume: Boolean = false, // 是否是卷名 + var baseUrl: String = "", // 用来拼接相对url + var bookUrl: String = "", // 书籍地址 + var index: Int = 0, // 章节序号 + var isVip: Boolean = false, // 是否VIP + var isPay: Boolean = false, // 是否已购买 + var resourceUrl: String? = null, // 音频真实URL + var tag: String? = null, // 更新时间或其他章节附加信息 + var wordCount: String? = null, // 本章节字数 + var start: Long? = null, // 章节起始位置 + var end: Long? = null, // 章节终止位置 + var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId + var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId + var variable: String? = null //变量 +) : Parcelable, RuleDataInterface { + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap: HashMap by lazy { + GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() + } + + @Ignore + @IgnoredOnParcel + var titleMD5: String? = null + + override fun putVariable(key: String, value: String?): Boolean { + if (super.putVariable(key, value)) { + variable = GSON.toJson(variableMap) + } + return true + } + + override fun putBigVariable(key: String, value: String?) { + RuleBigDataHelp.putChapterVariable(bookUrl, url, key, value) + } + + override fun getBigVariable(key: String): String? { + return RuleBigDataHelp.getChapterVariable(bookUrl, url, key) + } + + override fun hashCode() = url.hashCode() + + override fun equals(other: Any?): Boolean { + if (other is BookChapter) { + return other.url == url + } + return false + } + + fun primaryStr(): String { + return bookUrl + url + } + + fun getDisplayTitle( + replaceRules: List? = null, + useReplace: Boolean = true, + chineseConvert: Boolean = true, + ): String { + var displayTitle = title.replace(AppPattern.rnRegex, "") + if (chineseConvert) { + when (AppConfig.chineseConverterType) { + 1 -> displayTitle = ChineseUtils.t2s(displayTitle) + 2 -> displayTitle = ChineseUtils.s2t(displayTitle) + } + } + if (useReplace && replaceRules != null) kotlin.run { + replaceRules.forEach { item -> + if (item.pattern.isNotEmpty()) { + try { + val mDisplayTitle = if (item.isRegex) { + displayTitle.replace( + item.regex, + item.replacement, + item.getValidTimeoutMillisecond() + ) + } else { + displayTitle.replace(item.pattern, item.replacement) + } + if (mDisplayTitle.isNotBlank()) { + displayTitle = mDisplayTitle + } + } catch (e: RegexTimeoutException) { + item.isEnabled = false + appDb.replaceRuleDao.update(item) + } catch (e: CancellationException) { + return@run + } catch (e: Exception) { + AppLog.put("${item.name}替换出错\n替换内容\n${displayTitle}", e) + appCtx.toastOnUi("${item.name}替换出错") + } + } + } + } + return displayTitle + } + + fun getAbsoluteURL(): String { + //二级目录解析的卷链接为空 返回目录页的链接 + if (url.startsWith(title) && isVolume) return baseUrl + val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) + val urlBefore = if (urlMatcher.find()) url.substring(0, urlMatcher.start()) else url + val urlAbsoluteBefore = NetworkUtils.getAbsoluteURL(baseUrl, urlBefore) + return if (urlBefore.length == url.length) { + urlAbsoluteBefore + } else { + "$urlAbsoluteBefore," + url.substring(urlMatcher.end()) + } + } + + private fun ensureTitleMD5Init() { + if (titleMD5 == null) { + titleMD5 = MD5Utils.md5Encode16(title) + } + } + + @SuppressLint("DefaultLocale") + @Suppress("unused") + fun getFileName(suffix: String = "nb"): String { + ensureTitleMD5Init() + return String.format("%05d-%s.%s", index, titleMD5, suffix) + } + + @SuppressLint("DefaultLocale") + @Suppress("unused") + fun getFontName(): String { + ensureTitleMD5Init() + return String.format("%05d-%s.ttf", index, titleMD5) + } +} + diff --git a/app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt b/app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt new file mode 100755 index 000000000..64a838f42 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt @@ -0,0 +1,15 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import androidx.room.ColumnInfo +import kotlinx.parcelize.Parcelize + +@Parcelize +class BookChapterReview( + @ColumnInfo(defaultValue = "0") + var bookId: Long = 0, + var chapterId: Long = 0, + var summaryUrl: String = "", +): Parcelable { + +} diff --git a/app/src/main/java/io/legado/app/data/entities/BookGroup.kt b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt new file mode 100755 index 000000000..ac4b048b5 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt @@ -0,0 +1,75 @@ +package io.legado.app.data.entities + +import android.content.Context +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import kotlinx.parcelize.Parcelize + +@Suppress("ConstPropertyName") +@Parcelize +@Entity(tableName = "book_groups") +data class BookGroup( + @PrimaryKey + val groupId: Long = 0b1, + var groupName: String = "", + var cover: String? = null, + var order: Int = 0, + @ColumnInfo(defaultValue = "1") + var enableRefresh: Boolean = true, + @ColumnInfo(defaultValue = "1") + var show: Boolean = true, + @ColumnInfo(defaultValue = "-1") + var bookSort: Int = -1 +) : Parcelable { + + companion object { + const val IdRoot = -100L + const val IdAll = -1L + const val IdLocal = -2L + const val IdAudio = -3L + const val IdNetNone = -4L + const val IdLocalNone = -5L + const val IdError = -11L + } + + fun getManageName(context: Context): String { + return when (groupId) { + IdAll -> "$groupName(${context.getString(R.string.all)})" + IdAudio -> "$groupName(${context.getString(R.string.audio)})" + IdLocal -> "$groupName(${context.getString(R.string.local)})" + IdNetNone -> "$groupName(${context.getString(R.string.net_no_group)})" + IdLocalNone -> "$groupName(${context.getString(R.string.local_no_group)})" + IdError -> "$groupName(${context.getString(R.string.update_book_fail)})" + else -> groupName + } + } + + fun getRealBookSort(): Int { + if (bookSort < 0) { + return AppConfig.bookshelfSort + } + return bookSort + } + + override fun hashCode(): Int { + return groupId.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is BookGroup) { + return other.groupId == groupId + && other.groupName == groupName + && other.cover == cover + && other.bookSort == bookSort + && other.enableRefresh == enableRefresh + && other.show == show + && other.order == order + } + return false + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BookProgress.kt b/app/src/main/java/io/legado/app/data/entities/BookProgress.kt new file mode 100755 index 000000000..d5fd70d10 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BookProgress.kt @@ -0,0 +1,21 @@ +package io.legado.app.data.entities + +data class BookProgress( + val name: String, + val author: String, + val durChapterIndex: Int, + val durChapterPos: Int, + val durChapterTime: Long, + val durChapterTitle: String? +) { + + constructor(book: Book) : this( + name = book.name, + author = book.author, + durChapterIndex = book.durChapterIndex, + durChapterPos = book.durChapterPos, + durChapterTime = book.durChapterTime, + durChapterTitle = book.durChapterTitle + ) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BookSource.kt b/app/src/main/java/io/legado/app/data/entities/BookSource.kt new file mode 100755 index 000000000..49edf798d --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BookSource.kt @@ -0,0 +1,310 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import android.text.TextUtils +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import io.legado.app.constant.AppPattern +import io.legado.app.constant.BookSourceType +import io.legado.app.data.entities.rule.BookInfoRule +import io.legado.app.data.entities.rule.ContentRule +import io.legado.app.data.entities.rule.ExploreRule +import io.legado.app.data.entities.rule.ReviewRule +import io.legado.app.data.entities.rule.SearchRule +import io.legado.app.data.entities.rule.TocRule +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.splitNotBlank +import kotlinx.parcelize.Parcelize + +@Suppress("unused") +@Parcelize +@TypeConverters(BookSource.Converters::class) +@Entity( + tableName = "book_sources", + indices = [(Index(value = ["bookSourceUrl"], unique = false))] +) +data class BookSource( + // 地址,包括 http/https + @PrimaryKey + var bookSourceUrl: String = "", + // 名称 + var bookSourceName: String = "", + // 分组 + var bookSourceGroup: String? = null, + // 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站) + @BookSourceType.Type + var bookSourceType: Int = 0, + // 详情页url正则 + var bookUrlPattern: String? = null, + // 手动排序编号 + @ColumnInfo(defaultValue = "0") + var customOrder: Int = 0, + // 是否启用 + @ColumnInfo(defaultValue = "1") + var enabled: Boolean = true, + // 启用发现 + @ColumnInfo(defaultValue = "1") + var enabledExplore: Boolean = true, + // js库 + override var jsLib: String? = null, + // 启用okhttp CookieJAr 自动保存每次请求的cookie + @ColumnInfo(defaultValue = "0") + override var enabledCookieJar: Boolean? = true, + // 并发率 + override var concurrentRate: String? = null, + // 请求头 + override var header: String? = null, + // 登录地址 + override var loginUrl: String? = null, + // 登录UI + override var loginUi: String? = null, + // 登录检测js + var loginCheckJs: String? = null, + // 封面解密js + var coverDecodeJs: String? = null, + // 注释 + var bookSourceComment: String? = null, + // 自定义变量说明 + var variableComment: String? = null, + // 最后更新时间,用于排序 + var lastUpdateTime: Long = 0, + // 响应时间,用于排序 + var respondTime: Long = 180000L, + // 智能排序的权重 + var weight: Int = 0, + // 发现url + var exploreUrl: String? = null, + // 发现筛选规则 + var exploreScreen: String? = null, + // 发现规则 + var ruleExplore: ExploreRule? = null, + // 搜索url + var searchUrl: String? = null, + // 搜索规则 + var ruleSearch: SearchRule? = null, + // 书籍信息页规则 + var ruleBookInfo: BookInfoRule? = null, + // 目录页规则 + var ruleToc: TocRule? = null, + // 正文页规则 + var ruleContent: ContentRule? = null, + // 段评规则 + var ruleReview: ReviewRule? = null +) : Parcelable, BaseSource { + + override fun getTag(): String { + return bookSourceName + } + + override fun getKey(): String { + return bookSourceUrl + } + + override fun hashCode(): Int { + return bookSourceUrl.hashCode() + } + + override fun equals(other: Any?): Boolean { + return if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false + } + + fun getSearchRule(): SearchRule { + ruleSearch?.let { return it } + val rule = SearchRule() + ruleSearch = rule + return rule + } + + fun getExploreRule(): ExploreRule { + ruleExplore?.let { return it } + val rule = ExploreRule() + ruleExplore = rule + return rule + } + + fun getBookInfoRule(): BookInfoRule { + ruleBookInfo?.let { return it } + val rule = BookInfoRule() + ruleBookInfo = rule + return rule + } + + fun getTocRule(): TocRule { + ruleToc?.let { return it } + val rule = TocRule() + ruleToc = rule + return rule + } + + fun getContentRule(): ContentRule { + ruleContent?.let { return it } + val rule = ContentRule() + ruleContent = rule + return rule + } + +// fun getReviewRule(): ReviewRule { +// ruleReview?.let { return it } +// val rule = ReviewRule() +// ruleReview = rule +// return rule +// } + + fun getDisPlayNameGroup(): String { + return if (bookSourceGroup.isNullOrBlank()) { + bookSourceName + } else { + String.format("%s (%s)", bookSourceName, bookSourceGroup) + } + } + + fun addGroup(groups: String): BookSource { + bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex)) + bookSourceGroup = TextUtils.join(",", it) + } + if (bookSourceGroup.isNullOrBlank()) bookSourceGroup = groups + return this + } + + fun removeGroup(groups: String): BookSource { + bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet()) + bookSourceGroup = TextUtils.join(",", it) + } + return this + } + + fun hasGroup(group: String): Boolean { + bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + return it.indexOf(group) != -1 + } + return false + } + + fun removeInvalidGroups() { + removeGroup(getInvalidGroupNames()) + } + + fun removeErrorComment() { + bookSourceComment = bookSourceComment + ?.split("\n\n") + ?.filterNot { + it.startsWith("// Error: ") + }?.joinToString("\n") + } + + fun addErrorComment(e: Throwable) { + bookSourceComment = + "// Error: ${e.localizedMessage}" + if (bookSourceComment.isNullOrBlank()) + "" else "\n\n${bookSourceComment}" + } + + fun getCheckKeyword(default: String): String { + ruleSearch?.checkKeyWord?.let { + if (it.isNotBlank()) { + return it + } + } + return default + } + + fun getInvalidGroupNames(): String { + return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter { + "失效" in it || it == "校验超时" + }?.joinToString() ?: "" + } + + fun getDisplayVariableComment(otherComment: String): String { + return if (variableComment.isNullOrBlank()) { + otherComment + } else { + "${variableComment}\n$otherComment" + } + } + + fun equal(source: BookSource): Boolean { + return equal(bookSourceName, source.bookSourceName) + && equal(bookSourceUrl, source.bookSourceUrl) + && equal(bookSourceGroup, source.bookSourceGroup) + && bookSourceType == source.bookSourceType + && equal(bookUrlPattern, source.bookUrlPattern) + && equal(bookSourceComment, source.bookSourceComment) + && customOrder == source.customOrder + && enabled == source.enabled + && enabledExplore == source.enabledExplore + && enabledCookieJar == source.enabledCookieJar + && equal(variableComment, source.variableComment) + && equal(concurrentRate, source.concurrentRate) + && equal(jsLib, source.jsLib) + && equal(header, source.header) + && equal(loginUrl, source.loginUrl) + && equal(loginUi, source.loginUi) + && equal(loginCheckJs, source.loginCheckJs) + && equal(coverDecodeJs, source.coverDecodeJs) + && equal(exploreUrl, source.exploreUrl) + && equal(searchUrl, source.searchUrl) + && getSearchRule() == source.getSearchRule() + && getExploreRule() == source.getExploreRule() + && getBookInfoRule() == source.getBookInfoRule() + && getTocRule() == source.getTocRule() + && getContentRule() == source.getContentRule() + } + + private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) + + class Converters { + + @TypeConverter + fun exploreRuleToString(exploreRule: ExploreRule?): String = + GSON.toJson(exploreRule) + + @TypeConverter + fun stringToExploreRule(json: String?) = + GSON.fromJsonObject(json).getOrNull() + + @TypeConverter + fun searchRuleToString(searchRule: SearchRule?): String = + GSON.toJson(searchRule) + + @TypeConverter + fun stringToSearchRule(json: String?) = + GSON.fromJsonObject(json).getOrNull() + + @TypeConverter + fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String = + GSON.toJson(bookInfoRule) + + @TypeConverter + fun stringToBookInfoRule(json: String?) = + GSON.fromJsonObject(json).getOrNull() + + @TypeConverter + fun tocRuleToString(tocRule: TocRule?): String = + GSON.toJson(tocRule) + + @TypeConverter + fun stringToTocRule(json: String?) = + GSON.fromJsonObject(json).getOrNull() + + @TypeConverter + fun contentRuleToString(contentRule: ContentRule?): String = + GSON.toJson(contentRule) + + @TypeConverter + fun stringToContentRule(json: String?) = + GSON.fromJsonObject(json).getOrNull() + + @TypeConverter + fun stringToReviewRule(json: String?): ReviewRule? = null + + @TypeConverter + fun reviewRuleToString(reviewRule: ReviewRule?): String = "null" + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt b/app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt new file mode 100755 index 000000000..3fea92656 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/BookSourcePart.kt @@ -0,0 +1,81 @@ +package io.legado.app.data.entities + +import android.text.TextUtils +import androidx.room.DatabaseView +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.utils.splitNotBlank + + +@DatabaseView( + """select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore, + (loginUrl is not null and trim(loginUrl) <> '') hasLoginUrl, lastUpdateTime, respondTime, weight, + (exploreUrl is not null and trim(exploreUrl) <> '') hasExploreUrl + from book_sources""", + viewName = "book_sources_part" +) +data class BookSourcePart( + // 地址,包括 http/https + var bookSourceUrl: String = "", + // 名称 + var bookSourceName: String = "", + // 分组 + var bookSourceGroup: String? = null, + // 手动排序编号 + var customOrder: Int = 0, + // 是否启用 + var enabled: Boolean = true, + // 启用发现 + var enabledExplore: Boolean = true, + // 是否有登录地址 + var hasLoginUrl: Boolean = false, + // 最后更新时间,用于排序 + var lastUpdateTime: Long = 0, + // 响应时间,用于排序 + var respondTime: Long = 180000L, + // 智能排序的权重 + var weight: Int = 0, + // 是否有发现url + var hasExploreUrl: Boolean = false +) { + + override fun hashCode(): Int { + return bookSourceUrl.hashCode() + } + + override fun equals(other: Any?): Boolean { + return if (other is BookSourcePart) other.bookSourceUrl == bookSourceUrl else false + } + + fun getDisPlayNameGroup(): String { + return if (bookSourceGroup.isNullOrBlank()) { + bookSourceName + } else { + String.format("%s (%s)", bookSourceName, bookSourceGroup) + } + } + + fun getBookSource(): BookSource? { + return appDb.bookSourceDao.getBookSource(bookSourceUrl) + } + + fun addGroup(groups: String) { + bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex)) + bookSourceGroup = TextUtils.join(",", it) + } + if (bookSourceGroup.isNullOrBlank()) bookSourceGroup = groups + } + + fun removeGroup(groups: String) { + bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet()) + bookSourceGroup = TextUtils.join(",", it) + } + } + +} + +fun List.toBookSource(): List { + return mapNotNull { it.getBookSource() } +} diff --git a/app/src/main/java/io/legado/app/data/entities/Bookmark.kt b/app/src/main/java/io/legado/app/data/entities/Bookmark.kt new file mode 100755 index 000000000..109397042 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/Bookmark.kt @@ -0,0 +1,24 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity( + tableName = "bookmarks", + indices = [(Index(value = ["bookName", "bookAuthor"], unique = false))] +) +data class Bookmark( + @PrimaryKey + val time: Long = System.currentTimeMillis(), + val bookName: String = "", + val bookAuthor: String = "", + var chapterIndex: Int = 0, + var chapterPos: Int = 0, + var chapterName: String = "", + var bookText: String = "", + var content: String = "" +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Cache.kt b/app/src/main/java/io/legado/app/data/entities/Cache.kt new file mode 100755 index 000000000..f9687dea7 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/Cache.kt @@ -0,0 +1,13 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "caches", indices = [(Index(value = ["key"], unique = true))]) +data class Cache( + @PrimaryKey + val key: String = "", + var value: String? = null, + var deadline: Long = 0L +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Cookie.kt b/app/src/main/java/io/legado/app/data/entities/Cookie.kt new file mode 100755 index 000000000..8e12febf4 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/Cookie.kt @@ -0,0 +1,12 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "cookies", indices = [(Index(value = ["url"], unique = true))]) +data class Cookie( + @PrimaryKey + var url: String = "", + var cookie: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/DictRule.kt b/app/src/main/java/io/legado/app/data/entities/DictRule.kt new file mode 100755 index 000000000..eae1fb8bb --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/DictRule.kt @@ -0,0 +1,50 @@ +package io.legado.app.data.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl +import kotlin.coroutines.coroutineContext + +/** + * 字典规则 + */ +@Entity(tableName = "dictRules") +data class DictRule( + @PrimaryKey + var name: String = "", + var urlRule: String = "", + var showRule: String = "", + @ColumnInfo(defaultValue = "1") + var enabled: Boolean = true, + @ColumnInfo(defaultValue = "0") + var sortNumber: Int = 0 +) { + + override fun hashCode(): Int { + return name.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is DictRule) { + return name == other.name + } + return false + } + + /** + * 搜索字典 + */ + suspend fun search(word: String): String { + val analyzeUrl = AnalyzeUrl(urlRule, key = word, coroutineContext = coroutineContext) + val body = analyzeUrl.getStrResponseAwait().body + if (showRule.isBlank()) { + return body!! + } + val analyzeRule = AnalyzeRule().setCoroutineContext(coroutineContext) + return analyzeRule.getString(showRule, mContent = body) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/HttpTTS.kt b/app/src/main/java/io/legado/app/data/entities/HttpTTS.kt new file mode 100755 index 000000000..a79af166a --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/HttpTTS.kt @@ -0,0 +1,84 @@ +package io.legado.app.data.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.jayway.jsonpath.DocumentContext +import io.legado.app.utils.GSON +import io.legado.app.utils.jsonPath +import io.legado.app.utils.readLong +import io.legado.app.utils.readString + +/** + * 在线朗读引擎 + */ +@Entity(tableName = "httpTTS") +data class HttpTTS( + @PrimaryKey + val id: Long = System.currentTimeMillis(), + var name: String = "", + var url: String = "", + var contentType: String? = null, + @ColumnInfo(defaultValue = "0") + override var concurrentRate: String? = "0", + override var loginUrl: String? = null, + override var loginUi: String? = null, + override var header: String? = null, + override var jsLib: String? = null, + @ColumnInfo(defaultValue = "0") + override var enabledCookieJar: Boolean? = false, + var loginCheckJs: String? = null, + @ColumnInfo(defaultValue = "0") + var lastUpdateTime: Long = System.currentTimeMillis() +) : BaseSource { + + override fun getTag(): String { + return name + } + + override fun getKey(): String { + return "httpTts:$id" + } + + @Suppress("MemberVisibilityCanBePrivate") + companion object { + + fun fromJsonDoc(doc: DocumentContext): Result { + return kotlin.runCatching { + val loginUi = doc.read("$.loginUi") + HttpTTS( + id = doc.readLong("$.id") ?: System.currentTimeMillis(), + name = doc.readString("$.name")!!, + url = doc.readString("$.url")!!, + contentType = doc.readString("$.contentType"), + concurrentRate = doc.readString("$.concurrentRate"), + loginUrl = doc.readString("$.loginUrl"), + loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(), + header = doc.readString("$.header"), + loginCheckJs = doc.readString("$.loginCheckJs"), + lastUpdateTime = doc.readLong("$.lastUpdateTime") ?: System.currentTimeMillis() + ) + } + } + + fun fromJson(json: String): Result { + return fromJsonDoc(jsonPath.parse(json)) + } + + fun fromJsonArray(jsonArray: String): Result> { + return kotlin.runCatching { + val sources = arrayListOf() + val doc = jsonPath.parse(jsonArray).read>("$") + doc.forEach { + val jsonItem = jsonPath.parse(it) + fromJsonDoc(jsonItem).getOrThrow().let { source -> + sources.add(source) + } + } + return@runCatching sources + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/KeyboardAssist.kt b/app/src/main/java/io/legado/app/data/entities/KeyboardAssist.kt new file mode 100755 index 000000000..7cab22111 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/KeyboardAssist.kt @@ -0,0 +1,20 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import kotlinx.parcelize.Parcelize + + +@Parcelize +@Entity(tableName = "keyboardAssists", primaryKeys = ["type", "key"]) +data class KeyboardAssist( + @ColumnInfo(defaultValue = "0") + var type: Int = 0, + @ColumnInfo(defaultValue = "") + var key: String, + @ColumnInfo(defaultValue = "") + var value: String, + @ColumnInfo(defaultValue = "0") + var serialNo: Int = 0 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt b/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt new file mode 100755 index 000000000..c1638ac31 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/ReadRecord.kt @@ -0,0 +1,14 @@ +package io.legado.app.data.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity(tableName = "readRecord", primaryKeys = ["deviceId", "bookName"]) +data class ReadRecord( + var deviceId: String = "", + var bookName: String = "", + @ColumnInfo(defaultValue = "0") + var readTime: Long = 0L, + @ColumnInfo(defaultValue = "0") + var lastRead: Long = System.currentTimeMillis() +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt b/app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt new file mode 100755 index 000000000..293270276 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt @@ -0,0 +1,7 @@ +package io.legado.app.data.entities + +data class ReadRecordShow( + var bookName: String, + var readTime: Long, + var lastRead: Long +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt new file mode 100755 index 000000000..e3b87e31a --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt @@ -0,0 +1,121 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import android.text.TextUtils +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.exception.NoStackTraceException +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import splitties.init.appCtx +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +@Parcelize +@Entity( + tableName = "replace_rules", + indices = [(Index(value = ["id"]))] +) +data class ReplaceRule( + @PrimaryKey(autoGenerate = true) + var id: Long = System.currentTimeMillis(), + //名称 + @ColumnInfo(defaultValue = "") + var name: String = "", + //分组 + var group: String? = null, + //替换内容 + @ColumnInfo(defaultValue = "") + var pattern: String = "", + //替换为 + @ColumnInfo(defaultValue = "") + var replacement: String = "", + //作用范围 + var scope: String? = null, + //作用于标题 + @ColumnInfo(defaultValue = "0") + var scopeTitle: Boolean = false, + //作用于正文 + @ColumnInfo(defaultValue = "1") + var scopeContent: Boolean = true, + //排除范围 + var excludeScope: String? = null, + //是否启用 + @ColumnInfo(defaultValue = "1") + var isEnabled: Boolean = true, + //是否正则 + @ColumnInfo(defaultValue = "1") + var isRegex: Boolean = true, + //超时时间 + @ColumnInfo(defaultValue = "3000") + var timeoutMillisecond: Long = 3000L, + //排序 + @ColumnInfo(name = "sortOrder", defaultValue = "0") + var order: Int = Int.MIN_VALUE +) : Parcelable { + + override fun equals(other: Any?): Boolean { + if (other is ReplaceRule) { + return other.id == id + } + return super.equals(other) + } + + override fun hashCode(): Int { + return id.hashCode() + } + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + val regex: Regex by lazy { + pattern.toRegex() + } + + fun getDisplayNameGroup(): String { + return if (group.isNullOrBlank()) { + name + } else { + String.format("%s (%s)", name, group) + } + } + + fun isValid(): Boolean { + if (TextUtils.isEmpty(pattern)) { + return false + } + //判断正则表达式是否正确 + if (isRegex) { + try { + Pattern.compile(pattern) + } catch (ex: PatternSyntaxException) { + AppLog.put("正则语法错误或不支持:${ex.localizedMessage}", ex) + return false + } + // Pattern.compile测试通过,但是部分情况下会替换超时,报错,一般发生在修改表达式时漏删了 + if (pattern.endsWith('|') && !pattern.endsWith("\\|")) { + return false + } + } + return true + } + + @Throws(NoStackTraceException::class) + fun checkValid() { + if (!isValid()) { + throw NoStackTraceException(appCtx.getString(R.string.replace_rule_invalid)) + } + } + + fun getValidTimeoutMillisecond(): Long { + if (timeoutMillisecond <= 0) { + return 3000L + } + return timeoutMillisecond + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt new file mode 100755 index 000000000..a6aa1e639 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt @@ -0,0 +1,57 @@ +package io.legado.app.data.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.IgnoredOnParcel + +@Entity( + tableName = "rssArticles", + primaryKeys = ["origin", "link"] +) +data class RssArticle( + override var origin: String = "", + var sort: String = "", + var title: String = "", + var order: Long = 0, + override var link: String = "", + var pubDate: String? = null, + var description: String? = null, + var content: String? = null, + var image: String? = null, + @ColumnInfo(defaultValue = "默认分组") + var group: String = "默认分组", + var read: Boolean = false, + override var variable: String? = null +) : BaseRssArticle { + + override fun hashCode() = link.hashCode() + + override fun equals(other: Any?): Boolean { + other ?: return false + return if (other is RssArticle) origin == other.origin && link == other.link else false + } + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap: HashMap by lazy { + GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() + } + + fun toStar() = RssStar( + origin = origin, + sort = sort, + title = title, + starTime = System.currentTimeMillis(), + link = link, + pubDate = pubDate, + description = description, + content = content, + image = image, + group = group, + variable = variable + ) +} diff --git a/app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt b/app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt new file mode 100755 index 000000000..4fffca3e5 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt @@ -0,0 +1,13 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "rssReadRecords") +data class RssReadRecord( + @PrimaryKey + val record: String, + val title: String? = null, + val readTime: Long? = null, + val read: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssSource.kt b/app/src/main/java/io/legado/app/data/entities/RssSource.kt new file mode 100755 index 000000000..4057a9868 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RssSource.kt @@ -0,0 +1,182 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import android.text.TextUtils +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import io.legado.app.constant.AppPattern +import io.legado.app.utils.splitNotBlank +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))]) +data class RssSource( + @PrimaryKey + var sourceUrl: String = "", + // 名称 + var sourceName: String = "", + // 图标 + var sourceIcon: String = "", + // 分组 + var sourceGroup: String? = null, + // 注释 + var sourceComment: String? = null, + // 是否启用 + var enabled: Boolean = true, + // 自定义变量说明 + var variableComment: String? = null, + // js库 + override var jsLib: String? = null, + // 启用okhttp CookieJAr 自动保存每次请求的cookie + @ColumnInfo(defaultValue = "0") + override var enabledCookieJar: Boolean? = true, + /**并发率**/ + override var concurrentRate: String? = null, + /**请求头**/ + override var header: String? = null, + /**登录地址**/ + override var loginUrl: String? = null, + /**登录Ui**/ + override var loginUi: String? = null, + /**登录检测js**/ + var loginCheckJs: String? = null, + /**封面解密js**/ + var coverDecodeJs: String? = null, + /**分类Url**/ + var sortUrl: String? = null, + /**是否单url源**/ + var singleUrl: Boolean = false, + /*列表规则*/ + /**列表样式,0,1,2**/ + @ColumnInfo(defaultValue = "0") + var articleStyle: Int = 0, + /**列表规则**/ + var ruleArticles: String? = null, + /**下一页规则**/ + var ruleNextPage: String? = null, + /**标题规则**/ + var ruleTitle: String? = null, + /**发布日期规则**/ + var rulePubDate: String? = null, + /*webView规则*/ + /**描述规则**/ + var ruleDescription: String? = null, + /**图片规则**/ + var ruleImage: String? = null, + /**链接规则**/ + var ruleLink: String? = null, + /**正文规则**/ + var ruleContent: String? = null, + /**正文url白名单**/ + var contentWhitelist: String? = null, + /**正文url黑名单**/ + var contentBlacklist: String? = null, + /** + * 跳转url拦截, + * js, 返回true拦截,js变量url,可以通过js打开url,比如调用阅读搜索,添加书架等,简化规则写法,不用webView js注入 + * **/ + var shouldOverrideUrlLoading: String? = null, + /**webView样式**/ + var style: String? = null, + @ColumnInfo(defaultValue = "1") + var enableJs: Boolean = true, + @ColumnInfo(defaultValue = "1") + var loadWithBaseUrl: Boolean = true, + /**注入js**/ + var injectJs: String? = null, + /*其它规则*/ + /**最后更新时间,用于排序**/ + @ColumnInfo(defaultValue = "0") + var lastUpdateTime: Long = 0, + @ColumnInfo(defaultValue = "0") + var customOrder: Int = 0 +) : Parcelable, BaseSource { + + override fun getTag(): String { + return sourceName + } + + override fun getKey(): String { + return sourceUrl + } + + override fun equals(other: Any?): Boolean { + if (other is RssSource) { + return other.sourceUrl == sourceUrl + } + return false + } + + override fun hashCode() = sourceUrl.hashCode() + + fun equal(source: RssSource): Boolean { + return equal(sourceUrl, source.sourceUrl) + && equal(sourceName, source.sourceName) + && equal(sourceIcon, source.sourceIcon) + && enabled == source.enabled + && equal(sourceGroup, source.sourceGroup) + && enabledCookieJar == source.enabledCookieJar + && equal(sourceComment, source.sourceComment) + && equal(concurrentRate, source.concurrentRate) + && equal(header, source.header) + && equal(loginUrl, source.loginUrl) + && equal(loginUi, source.loginUi) + && equal(loginCheckJs, source.loginCheckJs) + && equal(coverDecodeJs, source.coverDecodeJs) + && equal(sortUrl, source.sortUrl) + && singleUrl == source.singleUrl + && articleStyle == source.articleStyle + && equal(ruleArticles, source.ruleArticles) + && equal(ruleNextPage, source.ruleNextPage) + && equal(ruleTitle, source.ruleTitle) + && equal(rulePubDate, source.rulePubDate) + && equal(ruleDescription, source.ruleDescription) + && equal(ruleLink, source.ruleLink) + && equal(ruleContent, source.ruleContent) + && enableJs == source.enableJs + && loadWithBaseUrl == source.loadWithBaseUrl + && equal(variableComment, source.variableComment) + && equal(style, source.style) + && equal(injectJs, source.injectJs) + } + + private fun equal(a: String?, b: String?): Boolean { + return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) + } + + fun getDisplayNameGroup(): String { + return if (sourceGroup.isNullOrBlank()) { + sourceName + } else { + String.format("%s (%s)", sourceName, sourceGroup) + } + } + + fun addGroup(groups: String): RssSource { + sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex)) + sourceGroup = TextUtils.join(",", it) + } + if (sourceGroup.isNullOrBlank()) sourceGroup = groups + return this + } + + fun removeGroup(groups: String): RssSource { + sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let { + it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet()) + sourceGroup = TextUtils.join(",", it) + } + return this + } + + fun getDisplayVariableComment(otherComment: String): String { + return if (variableComment.isNullOrBlank()) { + otherComment + } else { + "${variableComment}\n$otherComment" + } + } + +} diff --git a/app/src/main/java/io/legado/app/data/entities/RssStar.kt b/app/src/main/java/io/legado/app/data/entities/RssStar.kt new file mode 100755 index 000000000..f9a74109d --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RssStar.kt @@ -0,0 +1,49 @@ +package io.legado.app.data.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.IgnoredOnParcel + + +@Entity( + tableName = "rssStars", + primaryKeys = ["origin", "link"] +) +data class RssStar( + override var origin: String = "", + var sort: String = "", + var title: String = "", + var starTime: Long = 0, + override var link: String = "", + var pubDate: String? = null, + var description: String? = null, + var content: String? = null, + var image: String? = null, + @ColumnInfo(defaultValue = "默认分组") + var group: String = "默认分组", + override var variable: String? = null +) : BaseRssArticle { + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap by lazy { + GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() + } + + fun toRssArticle() = RssArticle( + origin = origin, + sort = sort, + title = title, + link = link, + pubDate = pubDate, + description = description, + content = content, + image = image, + group = group, + variable = variable + ) +} diff --git a/app/src/main/java/io/legado/app/data/entities/RuleSub.kt b/app/src/main/java/io/legado/app/data/entities/RuleSub.kt new file mode 100755 index 000000000..2b43787fe --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RuleSub.kt @@ -0,0 +1,16 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "ruleSubs") +data class RuleSub( + @PrimaryKey + val id: Long = System.currentTimeMillis(), + var name: String = "", + var url: String = "", + var type: Int = 0, + var customOrder: Int = 0, + var autoUpdate: Boolean = false, + var update: Long = System.currentTimeMillis() +) diff --git a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt new file mode 100755 index 000000000..2b327b6d7 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt @@ -0,0 +1,139 @@ +package io.legado.app.data.entities + +import android.content.Context +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import io.legado.app.R +import io.legado.app.constant.BookType +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity( + tableName = "searchBooks", + indices = [(Index(value = ["bookUrl"], unique = true)), + (Index(value = ["origin"], unique = false))], + foreignKeys = [(ForeignKey( + entity = BookSource::class, + parentColumns = ["bookSourceUrl"], + childColumns = ["origin"], + onDelete = ForeignKey.CASCADE + ))] +) +data class SearchBook( + @PrimaryKey + override var bookUrl: String = "", + /** 书源 */ + var origin: String = "", + var originName: String = "", + /** BookType */ + var type: Int = BookType.text, + override var name: String = "", + override var author: String = "", + override var kind: String? = null, + var coverUrl: String? = null, + var intro: String? = null, + override var wordCount: String? = null, + var latestChapterTitle: String? = null, + /** 目录页Url (toc=table of Contents) */ + var tocUrl: String = "", + var time: Long = System.currentTimeMillis(), + override var variable: String? = null, + var originOrder: Int = 0, + var chapterWordCountText: String? = null, + @ColumnInfo(defaultValue = "-1") + var chapterWordCount: Int = -1, + @ColumnInfo(defaultValue = "-1") + var respondTime: Int = -1 +) : Parcelable, BaseBook, Comparable { + + @Ignore + @IgnoredOnParcel + override var infoHtml: String? = null + + @Ignore + @IgnoredOnParcel + override var tocHtml: String? = null + + override fun equals(other: Any?) = other is SearchBook && other.bookUrl == bookUrl + + override fun hashCode() = bookUrl.hashCode() + + override fun compareTo(other: SearchBook): Int { + return other.originOrder - this.originOrder + } + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + override val variableMap: HashMap by lazy { + GSON.fromJsonObject>(variable).getOrNull() ?: HashMap() + } + + @delegate:Transient + @delegate:Ignore + @IgnoredOnParcel + val origins: LinkedHashSet by lazy { linkedSetOf(origin) } + + fun addOrigin(origin: String) { + origins.add(origin) + } + + fun getDisplayLastChapterTitle(): String { + latestChapterTitle?.let { + if (it.isNotEmpty()) { + return it + } + } + return "无最新章节" + } + + fun trimIntro(context: Context): String { + val trimIntro = intro?.trim() + return if (trimIntro.isNullOrEmpty()) { + context.getString(R.string.intro_show_null) + } else { + context.getString(R.string.intro_show, trimIntro) + } + } + + fun releaseHtmlData() { + infoHtml = null + tocHtml = null + } + + fun primaryStr(): String { + return origin + bookUrl + } + + fun sameBookTypeLocal(bookType: Int): Boolean { + return type and BookType.allBookTypeLocal == bookType and BookType.allBookTypeLocal + } + + fun toBook() = Book( + name = name, + author = author, + kind = kind, + bookUrl = bookUrl, + origin = origin, + originName = originName, + type = type, + wordCount = wordCount, + latestChapterTitle = latestChapterTitle, + coverUrl = coverUrl, + intro = intro, + tocUrl = tocUrl, + originOrder = originOrder, + variable = variable + ).apply { + this.infoHtml = this@SearchBook.infoHtml + this.tocHtml = this@SearchBook.tocHtml + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt b/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt new file mode 100755 index 000000000..6d46eb900 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/SearchKeyword.kt @@ -0,0 +1,20 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + + +@Parcelize +@Entity(tableName = "search_keywords", indices = [(Index(value = ["word"], unique = true))]) +data class SearchKeyword( + /** 搜索关键词 */ + @PrimaryKey + var word: String = "", + /** 使用次数 */ + var usage: Int = 1, + /** 最后一次使用时间 */ + var lastUseTime: Long = System.currentTimeMillis() +) : Parcelable diff --git a/app/src/main/java/io/legado/app/data/entities/Server.kt b/app/src/main/java/io/legado/app/data/entities/Server.kt new file mode 100755 index 000000000..c8d580729 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/Server.kt @@ -0,0 +1,57 @@ +package io.legado.app.data.entities + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.parcelize.Parcelize +import org.json.JSONObject + +/** + * 服务器 + */ +@Parcelize +@Entity(tableName = "servers") +data class Server( + @PrimaryKey + var id: Long = System.currentTimeMillis(), + var name: String = "", + var type: TYPE = TYPE.WEBDAV, + var config: String? = null, + var sortNumber: Int = 0 +) : Parcelable { + + enum class TYPE { + WEBDAV + } + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is Server) { + return id == other.id + } + return false + } + + fun getConfigJsonObject(): JSONObject? { + val json = config + json ?: return null + return JSONObject(json) + } + + fun getWebDavConfig(): WebDavConfig? { + return if (type == TYPE.WEBDAV) GSON.fromJsonObject(config).getOrNull() else null + } + + @Parcelize + data class WebDavConfig( + var url: String, + var username: String, + var password: String + ) : Parcelable + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt b/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt new file mode 100755 index 000000000..e62651f96 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt @@ -0,0 +1,29 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "txtTocRules") +data class TxtTocRule( + @PrimaryKey + var id: Long = System.currentTimeMillis(), + var name: String = "", + var rule: String = "", + var example: String? = null, + var serialNumber: Int = -1, + var enable: Boolean = true +) { + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is TxtTocRule) { + return id == other.id + } + return false + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt new file mode 100755 index 000000000..54fd63900 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt @@ -0,0 +1,42 @@ +package io.legado.app.data.entities.rule + +import android.os.Parcelable +import com.google.gson.JsonDeserializer +import io.legado.app.utils.INITIAL_GSON +import kotlinx.parcelize.Parcelize + +/** + * 书籍详情页规则 + */ +@Parcelize +data class BookInfoRule( + var init: String? = null, + var name: String? = null, + var author: String? = null, + var intro: String? = null, + var kind: String? = null, + var lastChapter: String? = null, + var updateTime: String? = null, + var coverUrl: String? = null, + var tocUrl: String? = null, + var wordCount: String? = null, + var canReName: String? = null, + var downloadUrls: String? = null +) : Parcelable { + + companion object { + + val jsonDeserializer = JsonDeserializer { json, _, _ -> + when { + json.isJsonObject -> INITIAL_GSON.fromJson(json, BookInfoRule::class.java) + json.isJsonPrimitive -> INITIAL_GSON.fromJson( + json.asString, + BookInfoRule::class.java + ) + else -> null + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/BookListRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/BookListRule.kt new file mode 100755 index 000000000..60015b998 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/BookListRule.kt @@ -0,0 +1,17 @@ +package io.legado.app.data.entities.rule + +/** + * 书籍列表规则 + */ +interface BookListRule { + var bookList: String? + var name: String? + var author: String? + var intro: String? + var kind: String? + var lastChapter: String? + var updateTime: String? + var bookUrl: String? + var coverUrl: String? + var wordCount: String? +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt new file mode 100755 index 000000000..aef453668 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt @@ -0,0 +1,41 @@ +package io.legado.app.data.entities.rule + +import android.os.Parcelable +import com.google.gson.JsonDeserializer +import io.legado.app.utils.INITIAL_GSON +import kotlinx.parcelize.Parcelize + +/** + * 正文处理规则 + */ +@Parcelize +data class ContentRule( + var content: String? = null, + var title: String? = null, //有些网站只能在正文中获取标题 + var nextContentUrl: String? = null, + var webJs: String? = null, + var sourceRegex: String? = null, + var replaceRegex: String? = null, //替换规则 + var imageStyle: String? = null, //默认大小居中,FULL最大宽度 + var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes + var payAction: String? = null, //购买操作,js或者包含{{js}}的url +) : Parcelable { + + + companion object { + + val jsonDeserializer = JsonDeserializer { json, _, _ -> + when { + json.isJsonObject -> INITIAL_GSON.fromJson(json, ContentRule::class.java) + json.isJsonPrimitive -> INITIAL_GSON.fromJson( + json.asString, + ContentRule::class.java + ) + else -> null + } + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ExploreKind.kt b/app/src/main/java/io/legado/app/data/entities/rule/ExploreKind.kt new file mode 100755 index 000000000..802fd31d8 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/ExploreKind.kt @@ -0,0 +1,16 @@ +package io.legado.app.data.entities.rule + +/** + * 发现分类 + */ +data class ExploreKind( + val title: String = "", + val url: String? = null, + val style: FlexChildStyle? = null +) { + + fun style(): FlexChildStyle { + return style ?: FlexChildStyle.defaultStyle + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt new file mode 100755 index 000000000..6871515ea --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt @@ -0,0 +1,40 @@ +package io.legado.app.data.entities.rule + +import android.os.Parcelable +import com.google.gson.JsonDeserializer +import io.legado.app.utils.INITIAL_GSON +import kotlinx.parcelize.Parcelize + +/** + * 发现结果规则 + */ +@Parcelize +data class ExploreRule( + override var bookList: String? = null, + override var name: String? = null, + override var author: String? = null, + override var intro: String? = null, + override var kind: String? = null, + override var lastChapter: String? = null, + override var updateTime: String? = null, + override var bookUrl: String? = null, + override var coverUrl: String? = null, + override var wordCount: String? = null +) : BookListRule, Parcelable { + + companion object { + + val jsonDeserializer = JsonDeserializer { json, _, _ -> + when { + json.isJsonObject -> INITIAL_GSON.fromJson(json, ExploreRule::class.java) + json.isJsonPrimitive -> INITIAL_GSON.fromJson( + json.asString, + ExploreRule::class.java + ) + else -> null + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/FlexChildStyle.kt b/app/src/main/java/io/legado/app/data/entities/rule/FlexChildStyle.kt new file mode 100755 index 000000000..7efa75055 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/FlexChildStyle.kt @@ -0,0 +1,39 @@ +package io.legado.app.data.entities.rule + +import android.view.View +import com.google.android.flexbox.FlexboxLayout + +data class FlexChildStyle( + val layout_flexGrow: Float = 0F, + val layout_flexShrink: Float = 1F, + val layout_alignSelf: String = "auto", + val layout_flexBasisPercent: Float = -1F, + val layout_wrapBefore: Boolean = false, +) { + + fun alignSelf(): Int { + return when (layout_alignSelf) { + "auto" -> -1 + "flex_start" -> 0 + "flex_end" -> 1 + "center" -> 2 + "baseline" -> 3 + "stretch" -> 4 + else -> -1 + } + } + + fun apply(view: View) { + val lp = view.layoutParams as FlexboxLayout.LayoutParams + lp.flexGrow = layout_flexGrow + lp.flexShrink = layout_flexShrink + lp.alignSelf = alignSelf() + lp.flexBasisPercent = layout_flexBasisPercent + lp.isWrapBefore = layout_wrapBefore + } + + companion object { + val defaultStyle = FlexChildStyle() + } + +} diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt new file mode 100755 index 000000000..ad9953439 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt @@ -0,0 +1,36 @@ +package io.legado.app.data.entities.rule + +import android.os.Parcelable +import com.google.gson.JsonDeserializer +import io.legado.app.utils.INITIAL_GSON +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ReviewRule( + var reviewUrl: String? = null, // 段评URL + var avatarRule: String? = null, // 段评发布者头像 + var contentRule: String? = null, // 段评内容 + var postTimeRule: String? = null, // 段评发布时间 + var reviewQuoteUrl: String? = null, // 获取段评回复URL + + // 这些功能将在以上功能完成以后实现 + var voteUpUrl: String? = null, // 点赞URL + var voteDownUrl: String? = null, // 点踩URL + var postReviewUrl: String? = null, // 发送回复URL + var postQuoteUrl: String? = null, // 发送回复段评URL + var deleteUrl: String? = null, // 删除段评URL +) : Parcelable { + + companion object { + + val jsonDeserializer = JsonDeserializer { json, _, _ -> + when { + json.isJsonObject -> INITIAL_GSON.fromJson(json, ReviewRule::class.java) + json.isJsonPrimitive -> INITIAL_GSON.fromJson(json.asString, ReviewRule::class.java) + else -> null + } + } + + } + +} diff --git a/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt b/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt new file mode 100755 index 000000000..8946707eb --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt @@ -0,0 +1,23 @@ +package io.legado.app.data.entities.rule + +data class RowUi( + var name: String = "", + var type: String = "text", + var action: String? = null, + var style: FlexChildStyle? = null +) { + + @Suppress("ConstPropertyName") + object Type { + + const val text = "text" + const val password = "password" + const val button = "button" + + } + + fun style(): FlexChildStyle { + return style ?: FlexChildStyle.defaultStyle + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt new file mode 100755 index 000000000..ba8cda454 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt @@ -0,0 +1,39 @@ +package io.legado.app.data.entities.rule + +import android.os.Parcelable +import com.google.gson.JsonDeserializer +import io.legado.app.utils.INITIAL_GSON +import kotlinx.parcelize.Parcelize + +/** + * 搜索结果处理规则 + */ +@Parcelize +data class SearchRule( + /**校验关键字**/ + var checkKeyWord: String? = null, + override var bookList: String? = null, + override var name: String? = null, + override var author: String? = null, + override var intro: String? = null, + override var kind: String? = null, + override var lastChapter: String? = null, + override var updateTime: String? = null, + override var bookUrl: String? = null, + override var coverUrl: String? = null, + override var wordCount: String? = null +) : BookListRule, Parcelable { + + companion object { + + val jsonDeserializer = JsonDeserializer { json, _, _ -> + when { + json.isJsonObject -> INITIAL_GSON.fromJson(json, SearchRule::class.java) + json.isJsonPrimitive -> INITIAL_GSON.fromJson(json.asString, SearchRule::class.java) + else -> null + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt new file mode 100755 index 000000000..f525e9123 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt @@ -0,0 +1,34 @@ +package io.legado.app.data.entities.rule + +import android.os.Parcelable +import com.google.gson.JsonDeserializer +import io.legado.app.utils.INITIAL_GSON +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TocRule( + var preUpdateJs: String? = null, + var chapterList: String? = null, + var chapterName: String? = null, + var chapterUrl: String? = null, + var formatJs: String? = null, + var isVolume: String? = null, + var isVip: String? = null, + var isPay: String? = null, + var updateTime: String? = null, + var nextTocUrl: String? = null +) : Parcelable { + + companion object { + + val jsonDeserializer = JsonDeserializer { json, _, _ -> + when { + json.isJsonObject -> INITIAL_GSON.fromJson(json, TocRule::class.java) + json.isJsonPrimitive -> INITIAL_GSON.fromJson(json.asString, TocRule::class.java) + else -> null + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/ConcurrentException.kt b/app/src/main/java/io/legado/app/exception/ConcurrentException.kt new file mode 100755 index 000000000..0ec3d6cca --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/ConcurrentException.kt @@ -0,0 +1,8 @@ +@file:Suppress("unused") + +package io.legado.app.exception + +/** + * 并发限制 + */ +class ConcurrentException(msg: String, val waitTime: Int) : NoStackTraceException(msg) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/ContentEmptyException.kt b/app/src/main/java/io/legado/app/exception/ContentEmptyException.kt new file mode 100755 index 000000000..cf8ad3067 --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/ContentEmptyException.kt @@ -0,0 +1,6 @@ +package io.legado.app.exception + +/** + * 内容为空 + */ +class ContentEmptyException(msg: String) : NoStackTraceException(msg) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/EmptyFileException.kt b/app/src/main/java/io/legado/app/exception/EmptyFileException.kt new file mode 100755 index 000000000..034cc2880 --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/EmptyFileException.kt @@ -0,0 +1,6 @@ +package io.legado.app.exception + +/** + * 文件为空 + */ +class EmptyFileException(msg: String) : NoStackTraceException(msg) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/InvalidBooksDirException.kt b/app/src/main/java/io/legado/app/exception/InvalidBooksDirException.kt new file mode 100755 index 000000000..49afbb8b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/InvalidBooksDirException.kt @@ -0,0 +1,3 @@ +package io.legado.app.exception + +class InvalidBooksDirException(msg: String) : NoStackTraceException(msg) diff --git a/app/src/main/java/io/legado/app/exception/NoBooksDirException.kt b/app/src/main/java/io/legado/app/exception/NoBooksDirException.kt new file mode 100755 index 000000000..d53e89b42 --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/NoBooksDirException.kt @@ -0,0 +1,6 @@ +package io.legado.app.exception + +import io.legado.app.R +import splitties.init.appCtx + +class NoBooksDirException: NoStackTraceException(appCtx.getString(R.string.no_books_dir)) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/NoStackTraceException.kt b/app/src/main/java/io/legado/app/exception/NoStackTraceException.kt new file mode 100755 index 000000000..03e469283 --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/NoStackTraceException.kt @@ -0,0 +1,17 @@ +package io.legado.app.exception + +/** + * 不记录错误堆栈的报错 + */ +open class NoStackTraceException(msg: String) : Exception(msg) { + + override fun fillInStackTrace(): Throwable { + stackTrace = emptyStackTrace + return this + } + + companion object { + private val emptyStackTrace = emptyArray() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/RegexTimeoutException.kt b/app/src/main/java/io/legado/app/exception/RegexTimeoutException.kt new file mode 100755 index 000000000..fcbdb5a8f --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/RegexTimeoutException.kt @@ -0,0 +1,3 @@ +package io.legado.app.exception + +class RegexTimeoutException(msg: String) : NoStackTraceException(msg) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/exception/TocEmptyException.kt b/app/src/main/java/io/legado/app/exception/TocEmptyException.kt new file mode 100755 index 000000000..d0b7a8d79 --- /dev/null +++ b/app/src/main/java/io/legado/app/exception/TocEmptyException.kt @@ -0,0 +1,6 @@ +package io.legado.app.exception + +/** + * 目录为空 + */ +class TocEmptyException(msg: String) : NoStackTraceException(msg) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/AppFreezeMonitor.kt b/app/src/main/java/io/legado/app/help/AppFreezeMonitor.kt new file mode 100755 index 000000000..59acb1520 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/AppFreezeMonitor.kt @@ -0,0 +1,80 @@ +package io.legado.app.help + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.os.HandlerThread +import android.os.SystemClock +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.LogUtils + +object AppFreezeMonitor { + + private const val TAG = "AppFreezeMonitor" + + val handler by lazy { + Handler(HandlerThread("AppFreezeMonitor").apply { start() }.looper) + } + + val screenStatusReceiver by lazy { + ScreenStatusReceiver() + } + + private var registeredReceiver = false + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + fun init(context: Context) { + if (!AppConfig.recordLog) { + if (registeredReceiver) { + registeredReceiver = false + context.unregisterReceiver(screenStatusReceiver) + } + return + } + + if (!registeredReceiver) { + registeredReceiver = true + context.registerReceiver(screenStatusReceiver, screenStatusReceiver.filter) + } + + var previous = SystemClock.uptimeMillis() + + val runnable = object : Runnable { + override fun run() { + val current = SystemClock.uptimeMillis() + val elapsed = current - previous + val extra = elapsed - 3000 + + if (extra > 300) { + LogUtils.d(TAG, "检测到应用被系统冻结,时长:$extra 毫秒") + } + + previous = current + + if (AppConfig.recordLog) { + handler.postDelayed(this, 3000) + } + } + } + handler.postDelayed(runnable, 3000) + } + + class ScreenStatusReceiver : BroadcastReceiver() { + + val filter = IntentFilter().apply { + addAction(Intent.ACTION_SCREEN_ON) + addAction(Intent.ACTION_SCREEN_OFF) + } + + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + Intent.ACTION_SCREEN_ON -> LogUtils.d(TAG, "SCREEN_ON") + Intent.ACTION_SCREEN_OFF -> LogUtils.d(TAG, "SCREEN_OFF") + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/AppWebDav.kt b/app/src/main/java/io/legado/app/help/AppWebDav.kt new file mode 100755 index 000000000..fa71a6fee --- /dev/null +++ b/app/src/main/java/io/legado/app/help/AppWebDav.kt @@ -0,0 +1,338 @@ +package io.legado.app.help + +import android.net.Uri +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookProgress +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.storage.Backup +import io.legado.app.help.storage.Restore +import io.legado.app.lib.webdav.Authorization +import io.legado.app.lib.webdav.WebDav +import io.legado.app.lib.webdav.WebDavException +import io.legado.app.lib.webdav.WebDavFile +import io.legado.app.model.remote.RemoteBookWebDav +import io.legado.app.utils.AlphanumComparator +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.UrlUtil +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getPrefString +import io.legado.app.utils.isJson +import io.legado.app.utils.normalizeFileName +import io.legado.app.utils.removePref +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.runBlocking +import splitties.init.appCtx +import java.io.File + +/** + * webDav初始化会访问网络,不要放到主线程 + */ +object AppWebDav { + private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" + private val bookProgressUrl get() = "${rootWebDavUrl}bookProgress/" + private val exportsWebDavUrl get() = "${rootWebDavUrl}books/" + private val bgWebDavUrl get() = "${rootWebDavUrl}background/" + + var authorization: Authorization? = null + private set + + var defaultBookWebDav: RemoteBookWebDav? = null + + val isOk get() = authorization != null + + val isJianGuoYun get() = rootWebDavUrl.startsWith(defaultWebDavUrl, true) + + init { + runBlocking { + upConfig() + } + } + + private val rootWebDavUrl: String + get() { + val configUrl = appCtx.getPrefString(PreferKey.webDavUrl)?.trim() + var url = if (configUrl.isNullOrEmpty()) defaultWebDavUrl else configUrl + if (!url.endsWith("/")) url = "${url}/" + AppConfig.webDavDir?.trim()?.let { + if (it.isNotEmpty()) { + url = "${url}${it}/" + } + } + return url + } + + suspend fun upConfig() { + kotlin.runCatching { + authorization = null + defaultBookWebDav = null + val account = appCtx.getPrefString(PreferKey.webDavAccount) + val password = appCtx.getPrefString(PreferKey.webDavPassword) + if (!account.isNullOrEmpty() && !password.isNullOrEmpty()) { + val mAuthorization = Authorization(account, password) + checkAuthorization(mAuthorization) + WebDav(rootWebDavUrl, mAuthorization).makeAsDir() + WebDav(bookProgressUrl, mAuthorization).makeAsDir() + WebDav(exportsWebDavUrl, mAuthorization).makeAsDir() + WebDav(bgWebDavUrl, mAuthorization).makeAsDir() + val rootBooksUrl = "${rootWebDavUrl}books/" + defaultBookWebDav = RemoteBookWebDav(rootBooksUrl, mAuthorization) + authorization = mAuthorization + } + } + } + + @Throws(WebDavException::class) + private suspend fun checkAuthorization(authorization: Authorization) { + if (!WebDav(rootWebDavUrl, authorization).check()) { + appCtx.removePref(PreferKey.webDavPassword) + appCtx.toastOnUi(R.string.webdav_application_authorization_error) + throw WebDavException(appCtx.getString(R.string.webdav_application_authorization_error)) + } + } + + @Throws(Exception::class) + suspend fun getBackupNames(): ArrayList { + val names = arrayListOf() + authorization?.let { + var files = WebDav(rootWebDavUrl, it).listFiles() + files = files.sortedWith { o1, o2 -> + AlphanumComparator.compare(o1.displayName, o2.displayName) + }.reversed() + files.forEach { webDav -> + val name = webDav.displayName + if (name.startsWith("backup")) { + names.add(name) + } + } + } ?: throw NoStackTraceException("webDav没有配置") + return names + } + + @Throws(WebDavException::class) + suspend fun restoreWebDav(name: String) { + authorization?.let { + val webDav = WebDav(rootWebDavUrl + name, it) + webDav.downloadTo(Backup.zipFilePath, true) + FileUtils.delete(Backup.backupPath) + ZipUtils.unZipToPath(File(Backup.zipFilePath), Backup.backupPath) + Restore.restoreLocked(Backup.backupPath) + } + } + + suspend fun hasBackUp(backUpName: String): Boolean { + authorization?.let { + val url = "$rootWebDavUrl${backUpName}" + return WebDav(url, it).exists() + } + return false + } + + suspend fun lastBackUp(): Result { + return kotlin.runCatching { + authorization?.let { + var lastBackupFile: WebDavFile? = null + WebDav(rootWebDavUrl, it).listFiles().reversed().forEach { webDavFile -> + if (webDavFile.displayName.startsWith("backup")) { + if (lastBackupFile == null + || webDavFile.lastModify > lastBackupFile.lastModify + ) { + lastBackupFile = webDavFile + } + } + } + lastBackupFile + } + } + } + + /** + * webDav备份 + * @param fileName 备份文件名 + */ + @Throws(Exception::class) + suspend fun backUpWebDav(fileName: String) { + if (!NetworkUtils.isAvailable()) return + authorization?.let { + val putUrl = "$rootWebDavUrl$fileName" + WebDav(putUrl, it).upload(Backup.zipFilePath) + } + } + + /** + * 获取云端所有背景名称 + */ + private suspend fun getAllBgWebDavFiles(): Result> { + return kotlin.runCatching { + if (!NetworkUtils.isAvailable()) + throw NoStackTraceException("网络未连接") + authorization.let { + it ?: throw NoStackTraceException("webDav未配置") + WebDav(bgWebDavUrl, it).listFiles() + } + } + } + + /** + * 上传背景图片 + */ + suspend fun upBgs(files: Array) { + val authorization = authorization ?: return + if (!NetworkUtils.isAvailable()) return + val bgWebDavFiles = getAllBgWebDavFiles().getOrThrow() + .map { it.displayName } + .toSet() + files.forEach { + if (!bgWebDavFiles.contains(it.name) && it.exists()) { + WebDav("$bgWebDavUrl${it.name}", authorization) + .upload(it) + } + } + } + + /** + * 下载背景图片 + */ + suspend fun downBgs() { + val authorization = authorization ?: return + if (!NetworkUtils.isAvailable()) return + val bgWebDavFiles = getAllBgWebDavFiles().getOrThrow() + .map { it.displayName } + .toSet() + } + + @Suppress("unused") + suspend fun exportWebDav(byteArray: ByteArray, fileName: String) { + if (!NetworkUtils.isAvailable()) return + try { + authorization?.let { + // 如果导出的本地文件存在,开始上传 + val putUrl = exportsWebDavUrl + fileName + WebDav(putUrl, it).upload(byteArray, "text/plain") + } + } catch (e: Exception) { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true) + } + } + + suspend fun exportWebDav(uri: Uri, fileName: String) { + if (!NetworkUtils.isAvailable()) return + try { + authorization?.let { + // 如果导出的本地文件存在,开始上传 + val putUrl = exportsWebDavUrl + fileName + WebDav(putUrl, it).upload(uri, "text/plain") + } + } catch (e: Exception) { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true) + } + } + + suspend fun uploadBookProgress( + book: Book, + toast: Boolean = false, + onSuccess: (() -> Unit)? = null + ) { + val authorization = authorization ?: return + if (!AppConfig.syncBookProgress) return + if (!NetworkUtils.isAvailable()) return + try { + val bookProgress = BookProgress(book) + val json = GSON.toJson(bookProgress) + val url = getProgressUrl(book.name, book.author) + WebDav(url, authorization).upload(json.toByteArray(), "application/json") + book.syncTime = System.currentTimeMillis() + onSuccess?.invoke() + } catch (e: Exception) { + currentCoroutineContext().ensureActive() + AppLog.put("上传进度失败\n${e.localizedMessage}", e, toast) + } + } + + suspend fun uploadBookProgress(bookProgress: BookProgress, onSuccess: (() -> Unit)? = null) { + try { + val authorization = authorization ?: return + if (!AppConfig.syncBookProgress) return + if (!NetworkUtils.isAvailable()) return + val json = GSON.toJson(bookProgress) + val url = getProgressUrl(bookProgress.name, bookProgress.author) + WebDav(url, authorization).upload(json.toByteArray(), "application/json") + onSuccess?.invoke() + } catch (e: Exception) { + currentCoroutineContext().ensureActive() + AppLog.put("上传进度失败\n${e.localizedMessage}", e) + } + } + + private fun getProgressUrl(name: String, author: String): String { + return bookProgressUrl + getProgressFileName(name, author) + } + + private fun getProgressFileName(name: String, author: String): String { + return UrlUtil.replaceReservedChar("${name}_${author}".normalizeFileName()) + ".json" + } + + /** + * 获取书籍进度 + */ + suspend fun getBookProgress(book: Book): BookProgress? { + val url = getProgressUrl(book.name, book.author) + kotlin.runCatching { + val authorization = authorization ?: return null + WebDav(url, authorization).download().let { byteArray -> + val json = String(byteArray) + if (json.isJson()) { + return GSON.fromJsonObject(json).getOrNull() + } + } + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("获取书籍进度失败\n${it.localizedMessage}", it) + } + return null + } + + suspend fun downloadAllBookProgress() { + val authorization = authorization ?: return + if (!NetworkUtils.isAvailable()) return + val bookProgressFiles = WebDav(bookProgressUrl, authorization).listFiles() + val map = hashMapOf() + bookProgressFiles.forEach { + map[it.displayName] = it + } + appDb.bookDao.all.forEach { book -> + val progressFileName = getProgressFileName(book.name, book.author) + val webDavFile = map[progressFileName] + webDavFile ?: return + if (webDavFile.lastModify <= book.syncTime) { + //本地同步时间大于上传时间不用同步 + return + } + getBookProgress(book)?.let { bookProgress -> + if (bookProgress.durChapterIndex > book.durChapterIndex + || (bookProgress.durChapterIndex == book.durChapterIndex + && bookProgress.durChapterPos > book.durChapterPos) + ) { + book.durChapterIndex = bookProgress.durChapterIndex + book.durChapterPos = bookProgress.durChapterPos + book.durChapterTitle = bookProgress.durChapterTitle + book.durChapterTime = bookProgress.durChapterTime + book.syncTime = System.currentTimeMillis() + appDb.bookDao.update(book) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/CacheManager.kt b/app/src/main/java/io/legado/app/help/CacheManager.kt new file mode 100755 index 000000000..4bc9e40e6 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/CacheManager.kt @@ -0,0 +1,123 @@ +package io.legado.app.help + +import androidx.annotation.Keep +import androidx.collection.LruCache +import io.legado.app.data.appDb +import io.legado.app.data.entities.Cache +import io.legado.app.model.analyzeRule.QueryTTF +import io.legado.app.utils.ACache +import io.legado.app.utils.memorySize + +private val queryTTFMap = LruCache(4) + +/** + * 最多只缓存50M的数据,防止OOM + */ +private val memoryLruCache = object : LruCache(1024 * 1024 * 50) { + + override fun sizeOf(key: String, value: Any): Int { + return value.toString().memorySize() + } + +} + +object AppCacheManager { + + fun put(key: String, queryTTF: QueryTTF) { + queryTTFMap.put(key, queryTTF) + } + + fun getQueryTTF(key: String): QueryTTF? { + return queryTTFMap[key] + } + + fun clearSourceVariables() { + memoryLruCache.snapshot().keys.forEach { + if (it.startsWith("v_") + || it.startsWith("userInfo_") + || it.startsWith("loginHeader_") + || it.startsWith("sourceVariable_") + ) { + memoryLruCache.remove(it) + } + } + } + +} + + +@Keep +@Suppress("unused") +object CacheManager { + + /** + * saveTime 单位为秒 + */ + @JvmOverloads + fun put(key: String, value: Any, saveTime: Int = 0) { + val deadline = + if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 + when (value) { + is ByteArray -> ACache.get().put(key, value, saveTime) + else -> { + val cache = Cache(key, value.toString(), deadline) + appDb.cacheDao.insert(cache) + } + } + } + + fun putMemory(key: String, value: Any) { + memoryLruCache.put(key, value) + } + + //从内存中获取数据 使用lruCache + fun getFromMemory(key: String): Any? { + return memoryLruCache[key] + } + + fun deleteMemory(key: String) { + memoryLruCache.remove(key) + } + + fun get(key: String): String? { + val cache = appDb.cacheDao.get(key) + if (cache != null && (cache.deadline == 0L || cache.deadline > System.currentTimeMillis())) { + return cache.value + } + return null + } + + fun getInt(key: String): Int? { + return get(key)?.toIntOrNull() + } + + fun getLong(key: String): Long? { + return get(key)?.toLongOrNull() + } + + fun getDouble(key: String): Double? { + return get(key)?.toDoubleOrNull() + } + + fun getFloat(key: String): Float? { + return get(key)?.toFloatOrNull() + } + + fun getByteArray(key: String): ByteArray? { + return ACache.get().getAsBinary(key) + } + + fun putFile(key: String, value: String, saveTime: Int = 0) { + ACache.get().put(key, value, saveTime) + } + + fun getFile(key: String): String? { + return ACache.get().getAsString(key) + } + + fun delete(key: String) { + appDb.cacheDao.delete(key) + deleteMemory(key) + ACache.get().remove(key) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ConcurrentRateLimiter.kt b/app/src/main/java/io/legado/app/help/ConcurrentRateLimiter.kt new file mode 100755 index 000000000..d36144e02 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ConcurrentRateLimiter.kt @@ -0,0 +1,135 @@ +package io.legado.app.help + +import io.legado.app.data.entities.BaseSource +import io.legado.app.exception.ConcurrentException +import io.legado.app.model.analyzeRule.AnalyzeUrl.ConcurrentRecord +import kotlinx.coroutines.delay + +class ConcurrentRateLimiter(val source: BaseSource?) { + + companion object { + private val concurrentRecordMap = hashMapOf() + } + + /** + * 开始访问,并发判断 + */ + @Throws(ConcurrentException::class) + private fun fetchStart(): ConcurrentRecord? { + source ?: return null + val concurrentRate = source.concurrentRate + if (concurrentRate.isNullOrEmpty() || concurrentRate == "0") { + return null + } + val rateIndex = concurrentRate.indexOf("/") + var fetchRecord = concurrentRecordMap[source.getKey()] + if (fetchRecord == null) { + synchronized(concurrentRecordMap) { + fetchRecord = concurrentRecordMap[source.getKey()] + if (fetchRecord == null) { + fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1) + concurrentRecordMap[source.getKey()] = fetchRecord + return fetchRecord + } + } + } + val waitTime: Int = synchronized(fetchRecord!!) { + try { + if (!fetchRecord.isConcurrent) { + //并发控制非 次数/毫秒 + if (fetchRecord.frequency > 0) { + //已经有访问线程,直接等待 + return@synchronized concurrentRate.toInt() + } + //没有线程访问,判断还剩多少时间可以访问 + val nextTime = fetchRecord.time + concurrentRate.toInt() + if (System.currentTimeMillis() >= nextTime) { + fetchRecord.time = System.currentTimeMillis() + fetchRecord.frequency = 1 + return@synchronized 0 + } + return@synchronized (nextTime - System.currentTimeMillis()).toInt() + } else { + //并发控制为 次数/毫秒 + val sj = concurrentRate.substring(rateIndex + 1) + val nextTime = fetchRecord.time + sj.toInt() + if (System.currentTimeMillis() >= nextTime) { + //已经过了限制时间,重置开始时间 + fetchRecord.time = System.currentTimeMillis() + fetchRecord.frequency = 1 + return@synchronized 0 + } + val cs = concurrentRate.substring(0, rateIndex) + if (fetchRecord.frequency > cs.toInt()) { + return@synchronized (nextTime - System.currentTimeMillis()).toInt() + } else { + fetchRecord.frequency += 1 + return@synchronized 0 + } + } + } catch (_: Exception) { + return@synchronized 0 + } + } + if (waitTime > 0) { + throw ConcurrentException( + "根据并发率还需等待${waitTime}毫秒才可以访问", + waitTime = waitTime + ) + } + return fetchRecord + } + + /** + * 访问结束 + */ + fun fetchEnd(concurrentRecord: ConcurrentRecord?) { + if (concurrentRecord != null && !concurrentRecord.isConcurrent) { + synchronized(concurrentRecord) { + concurrentRecord.frequency -= 1 + } + } + } + + /** + * 获取并发记录,若处于并发限制状态下则会等待 + */ + suspend fun getConcurrentRecord(): ConcurrentRecord? { + while (true) { + try { + return fetchStart() + } catch (e: ConcurrentException) { + delay(e.waitTime.toLong()) + } + } + } + + fun getConcurrentRecordBlocking(): ConcurrentRecord? { + while (true) { + try { + return fetchStart() + } catch (e: ConcurrentException) { + Thread.sleep(e.waitTime.toLong()) + } + } + } + + suspend inline fun withLimit(block: () -> T): T { + val concurrentRecord = getConcurrentRecord() + try { + return block() + } finally { + fetchEnd(concurrentRecord) + } + } + + inline fun withLimitBlocking(block: () -> T): T { + val concurrentRecord = getConcurrentRecordBlocking() + try { + return block() + } finally { + fetchEnd(concurrentRecord) + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/CrashHandler.kt b/app/src/main/java/io/legado/app/help/CrashHandler.kt new file mode 100755 index 000000000..f431be0a2 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/CrashHandler.kt @@ -0,0 +1,190 @@ +package io.legado.app.help + +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Debug +import android.os.Looper +import android.webkit.WebSettings +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.model.ReadAloud +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.createFolderReplace +import io.legado.app.utils.externalCache +import io.legado.app.utils.getFile +import io.legado.app.utils.longToastOnUiLegacy +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.writeText +import splitties.init.appCtx +import java.io.PrintWriter +import java.io.StringWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.concurrent.TimeUnit + +/** + * 异常管理类 + */ +class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler { + + /** + * 系统默认UncaughtExceptionHandler + */ + private var mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler() + + init { + //设置该CrashHandler为系统默认的 + Thread.setDefaultUncaughtExceptionHandler(this) + } + + /** + * uncaughtException 回调函数 + */ + override fun uncaughtException(thread: Thread, ex: Throwable) { + if (shouldAbsorb(ex)) { + AppLog.put("发生未捕获的异常\n${ex.localizedMessage}", ex) + Looper.loop() + } else { + ReadAloud.stop(context) + handleException(ex) + mDefaultHandler?.uncaughtException(thread, ex) + } + } + + private fun shouldAbsorb(e: Throwable): Boolean { + return when { + e::class.simpleName == "CannotDeliverBroadcastException" -> true + e is SecurityException && e.message?.contains( + "nor current process has android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS", + true + ) == true -> true + + else -> false + } + } + + /** + * 处理该异常 + */ + private fun handleException(ex: Throwable?) { + if (ex == null) return + LocalConfig.appCrash = true + //保存日志文件 + saveCrashInfo2File(ex) + if ((ex is OutOfMemoryError || ex.cause is OutOfMemoryError) && AppConfig.recordHeapDump) { + doHeapDump() + } + context.longToastOnUiLegacy(ex.stackTraceStr) + Thread.sleep(3000) + } + + companion object { + /** + * 存储异常和参数信息 + */ + private val paramsMap by lazy { + val map = LinkedHashMap() + kotlin.runCatching { + //获取系统信息 + map["MANUFACTURER"] = Build.MANUFACTURER + map["BRAND"] = Build.BRAND + map["MODEL"] = Build.MODEL + map["SDK_INT"] = Build.VERSION.SDK_INT.toString() + map["RELEASE"] = Build.VERSION.RELEASE + map["WebViewUserAgent"] = try { + WebSettings.getDefaultUserAgent(appCtx) + } catch (e: Throwable) { + e.toString() + } + map["packageName"] = appCtx.packageName + map["heapSize"] = Runtime.getRuntime().maxMemory().toString() + //获取app版本信息 + AppConst.appInfo.let { + map["versionName"] = it.versionName + map["versionCode"] = it.versionCode.toString() + } + } + map + } + + /** + * 格式化时间 + */ + @SuppressLint("SimpleDateFormat") + private val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss") + + /** + * 保存错误信息到文件中 + */ + fun saveCrashInfo2File(ex: Throwable) { + val sb = StringBuilder() + for ((key, value) in paramsMap) { + sb.append(key).append("=").append(value).append("\n") + } + + val writer = StringWriter() + val printWriter = PrintWriter(writer) + ex.printStackTrace(printWriter) + var cause: Throwable? = ex.cause + while (cause != null) { + cause.printStackTrace(printWriter) + cause = cause.cause + } + printWriter.close() + val result = writer.toString() + sb.append(result) + val crashLog = sb.toString() + val timestamp = System.currentTimeMillis() + val time = format.format(Date()) + val fileName = "crash-$time-$timestamp.log" + try { + val backupPath = AppConfig.backupPath + ?: throw NoStackTraceException("备份路径未配置") + val uri = Uri.parse(backupPath) + val fileDoc = FileDoc.fromUri(uri, true) + fileDoc.createFileIfNotExist(fileName, "crash") + .writeText(crashLog) + } catch (_: Exception) { + } + kotlin.runCatching { + appCtx.externalCacheDir?.let { rootFile -> + val exceedTimeMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7) + rootFile.getFile("crash").listFiles()?.forEach { + if (it.lastModified() < exceedTimeMillis) { + it.delete() + } + } + FileUtils.createFileIfNotExist(rootFile, "crash", fileName) + .writeText(crashLog) + } + } + } + + /** + * 进行堆转储 + */ + fun doHeapDump(manually: Boolean = false) { + val heapDir = appCtx + .externalCache + .getFile("heapDump") + heapDir.createFolderReplace() + val fileName = if (manually) { + "heap-dump-manually-${System.currentTimeMillis()}.hprof" + } else { + "heap-dump-${System.currentTimeMillis()}.hprof" + } + val heapFile = heapDir.getFile(fileName) + val heapDumpName = heapFile.absolutePath + Debug.dumpHprofData(heapDumpName) + } + + } + +} diff --git a/app/src/main/java/io/legado/app/help/DefaultData.kt b/app/src/main/java/io/legado/app/help/DefaultData.kt new file mode 100755 index 000000000..b99f7953b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/DefaultData.kt @@ -0,0 +1,132 @@ +package io.legado.app.help + +import io.legado.app.constant.AppConst +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.data.entities.HttpTTS +import io.legado.app.data.entities.KeyboardAssist +import io.legado.app.data.entities.RssSource +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.BookCover +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.printOnDebug +import splitties.init.appCtx +import java.io.File + +object DefaultData { + + fun upVersion() { + if (LocalConfig.versionCode < AppConst.appInfo.versionCode) { + Coroutine.async { + if (LocalConfig.needUpHttpTTS) { + importDefaultHttpTTS() + } + if (LocalConfig.needUpTxtTocRule) { + importDefaultTocRules() + } + if (LocalConfig.needUpRssSources) { + importDefaultRssSources() + } + if (LocalConfig.needUpDictRule) { + importDefaultDictRules() + } + }.onError { + it.printOnDebug() + } + } + } + + val httpTTS: List by lazy { + val json = + String( + appCtx.assets.open("defaultData${File.separator}httpTTS.json") + .readBytes() + ) + HttpTTS.fromJsonArray(json).getOrElse { + emptyList() + } + } + + val readConfigs: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}") + .readBytes() + ) + GSON.fromJsonArray(json).getOrNull() + ?: emptyList() + } + + val txtTocRules: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}txtTocRule.json") + .readBytes() + ) + GSON.fromJsonArray(json).getOrNull() ?: emptyList() + } + + val themeConfigs: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}") + .readBytes() + ) + GSON.fromJsonArray(json).getOrNull() ?: emptyList() + } + + val rssSources: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}rssSources.json") + .readBytes() + ) + GSON.fromJsonArray(json).getOrDefault(emptyList()) + } + + val coverRule: BookCover.CoverRule by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}coverRule.json") + .readBytes() + ) + GSON.fromJsonObject(json).getOrThrow() + } + + val dictRules: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}dictRules.json") + .readBytes() + ) + GSON.fromJsonArray(json).getOrThrow() + } + + val keyboardAssists: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}keyboardAssists.json") + .readBytes() + ) + GSON.fromJsonArray(json).getOrThrow() + } + + fun importDefaultHttpTTS() { + appDb.httpTTSDao.deleteDefault() + appDb.httpTTSDao.insert(*httpTTS.toTypedArray()) + } + + fun importDefaultTocRules() { + appDb.txtTocRuleDao.deleteDefault() + appDb.txtTocRuleDao.insert(*txtTocRules.toTypedArray()) + } + + fun importDefaultRssSources() { + appDb.rssSourceDao.deleteDefault() + appDb.rssSourceDao.insert(*rssSources.toTypedArray()) + } + + fun importDefaultDictRules() { + appDb.dictRuleDao.insert(*dictRules.toTypedArray()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/DirectLinkUpload.kt b/app/src/main/java/io/legado/app/help/DirectLinkUpload.kt new file mode 100755 index 000000000..90ab1327d --- /dev/null +++ b/app/src/main/java/io/legado/app/help/DirectLinkUpload.kt @@ -0,0 +1,116 @@ +package io.legado.app.help + +import androidx.annotation.Keep +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.ACache +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.createFileReplace +import io.legado.app.utils.externalCache +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import splitties.init.appCtx +import java.io.File +import kotlin.coroutines.coroutineContext + +@Suppress("MemberVisibilityCanBePrivate") +object DirectLinkUpload { + + const val ruleFileName = "directLinkUploadRule.json" + + @Throws(NoStackTraceException::class) + suspend fun upLoad( + fileName: String, + file: Any, + contentType: String, + rule: Rule = getRule() + ): String { + val url = rule.uploadUrl + if (url.isBlank()) { + throw NoStackTraceException("上传url未配置") + } + val downloadUrlRule = rule.downloadUrlRule + if (downloadUrlRule.isBlank()) { + throw NoStackTraceException("下载地址规则未配置") + } + var mFileName = fileName + var mFile = file + var mContentType = contentType + if (rule.compress && contentType != "application/zip") { + mFileName = "$fileName.zip" + mContentType = "application/zip" + mFile = when (file) { + is File -> { + val zipFile = File(FileUtils.getPath(appCtx.externalCache, "upload", mFileName)) + zipFile.createFileReplace() + ZipUtils.zipFile(file, zipFile) + zipFile + } + + is ByteArray -> ZipUtils.zipByteArray(file, fileName) + is String -> ZipUtils.zipByteArray(file.toByteArray(), fileName) + else -> ZipUtils.zipByteArray(GSON.toJson(file).toByteArray(), fileName) + } + } + val analyzeUrl = AnalyzeUrl(url) + val res = analyzeUrl.upload(mFileName, mFile, mContentType) + if (mFile is File) { + mFile.delete() + } + val analyzeRule = AnalyzeRule().setContent(res.body, res.url) + .setCoroutineContext(coroutineContext) + val downloadUrl = analyzeRule.getString(downloadUrlRule) + if (downloadUrl.isBlank()) { + throw NoStackTraceException("上传失败,${res.body}") + } + return downloadUrl + } + + val defaultRules: List by lazy { + val json = String( + appCtx.assets.open("defaultData${File.separator}directLinkUpload.json") + .readBytes() + ) + GSON.fromJsonArray(json).getOrThrow() + } + + fun getRule(): Rule { + return getConfig() ?: defaultRules[0] + } + + fun getConfig(): Rule? { + val json = ACache.get(cacheDir = false).getAsString(ruleFileName) + return GSON.fromJsonObject(json).getOrNull() + } + + fun putConfig(rule: Rule) { + ACache.get(cacheDir = false).put(ruleFileName, GSON.toJson(rule)) + } + + fun delConfig() { + ACache.get(cacheDir = false).remove(ruleFileName) + } + + fun getSummary(): String { + return getRule().summary + } + + @Keep + data class Rule( + var uploadUrl: String, //创建分享链接 + var downloadUrlRule: String, //下载链接规则 + var summary: String, //注释 + var compress: Boolean = false, //是否压缩 + ) { + + override fun toString(): String { + return summary + } + + } + +} diff --git a/app/src/main/java/io/legado/app/help/DispatchersMonitor.kt b/app/src/main/java/io/legado/app/help/DispatchersMonitor.kt new file mode 100755 index 000000000..49d418760 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/DispatchersMonitor.kt @@ -0,0 +1,59 @@ +package io.legado.app.help + +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.LogUtils +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.Default +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.selects.onTimeout +import kotlinx.coroutines.selects.select +import kotlinx.coroutines.withContext +import java.util.concurrent.Executors + +object DispatchersMonitor { + + private const val TAG = "DispatchersMonitor" + + private val dispatcher by lazy { + Executors.newSingleThreadExecutor { + Thread(it, TAG) + }.asCoroutineDispatcher() + } + + private val scope = CoroutineScope(dispatcher) + + fun init() { + scope.coroutineContext.cancelChildren() + if (!AppConfig.recordLog) { + return + } + monitor(IO) + monitor(Default) + monitor(Main) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun monitor(dispatcher: CoroutineDispatcher) { + scope.launch { + while (isActive) select { + launch { + withContext(dispatcher) { + delay(3000) + } + }.onJoin {} + onTimeout(5000) { + LogUtils.d(TAG, "Dispatcher $dispatcher is timed out waiting for for 5000ms.") + } + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/EventMessage.kt b/app/src/main/java/io/legado/app/help/EventMessage.kt new file mode 100755 index 000000000..31e8f662e --- /dev/null +++ b/app/src/main/java/io/legado/app/help/EventMessage.kt @@ -0,0 +1,49 @@ +package io.legado.app.help + +import android.text.TextUtils + +@Suppress("unused") +class EventMessage { + + var what: Int? = null + var tag: String? = null + var obj: Any? = null + + fun isFrom(tag: String): Boolean { + return TextUtils.equals(this.tag, tag) + } + + fun maybeFrom(vararg tags: String): Boolean { + return listOf(*tags).contains(tag) + } + + companion object { + + fun obtain(tag: String): EventMessage { + val message = EventMessage() + message.tag = tag + return message + } + + fun obtain(what: Int): EventMessage { + val message = EventMessage() + message.what = what + return message + } + + fun obtain(what: Int, obj: Any): EventMessage { + val message = EventMessage() + message.what = what + message.obj = obj + return message + } + + fun obtain(tag: String, obj: Any): EventMessage { + val message = EventMessage() + message.tag = tag + message.obj = obj + return message + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/ExecutorService.kt b/app/src/main/java/io/legado/app/help/ExecutorService.kt new file mode 100755 index 000000000..78f6f2f95 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ExecutorService.kt @@ -0,0 +1,6 @@ +package io.legado.app.help + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +val globalExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() } diff --git a/app/src/main/java/io/legado/app/help/IntentData.kt b/app/src/main/java/io/legado/app/help/IntentData.kt new file mode 100755 index 000000000..474a88bc8 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/IntentData.kt @@ -0,0 +1,32 @@ +package io.legado.app.help + +object IntentData { + + private val bigData: MutableMap = mutableMapOf() + + @Synchronized + fun put(key: String, data: Any?): String { + data?.let { + bigData[key] = data + } + return key + } + + @Synchronized + fun put(data: Any?): String { + val key = System.currentTimeMillis().toString() + data?.let { + bigData[key] = data + } + return key + } + + @Suppress("UNCHECKED_CAST") + @Synchronized + fun get(key: String?): T? { + if (key == null) return null + val data = bigData[key] + bigData.remove(key) + return data as? T + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/IntentHelp.kt b/app/src/main/java/io/legado/app/help/IntentHelp.kt new file mode 100755 index 000000000..5885e46c6 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/IntentHelp.kt @@ -0,0 +1,50 @@ +package io.legado.app.help + +import android.content.Context +import android.content.Intent +import android.net.Uri +import io.legado.app.R +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +@Suppress("unused") +object IntentHelp { + + fun getBrowserIntent(url: String): Intent { + return getBrowserIntent(Uri.parse(url)) + } + + fun getBrowserIntent(uri: Uri): Intent { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = uri + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (intent.resolveActivity(appCtx.packageManager) == null) { + return Intent.createChooser(intent, "请选择浏览器") + } + return intent + } + + fun openTTSSetting() { + //跳转到文字转语音设置界面 + kotlin.runCatching { + val intent = Intent() + intent.action = "com.android.settings.TTS_SETTINGS" + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + appCtx.startActivity(intent) + }.onFailure { + appCtx.toastOnUi(R.string.tip_cannot_jump_setting_page) + } + } + + fun toInstallUnknown(context: Context) { + kotlin.runCatching { + val intent = Intent() + intent.action = "android.settings.MANAGE_UNKNOWN_APP_SOURCES" + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + }.onFailure { + context.toastOnUi("无法打开设置") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/JsEncodeUtils.kt b/app/src/main/java/io/legado/app/help/JsEncodeUtils.kt new file mode 100755 index 000000000..bdba435f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/JsEncodeUtils.kt @@ -0,0 +1,497 @@ +package io.legado.app.help + +import android.util.Base64 +import cn.hutool.crypto.digest.DigestUtil +import cn.hutool.crypto.digest.HMac +import cn.hutool.crypto.symmetric.SymmetricCrypto +import io.legado.app.help.crypto.AsymmetricCrypto +import io.legado.app.help.crypto.Sign +import io.legado.app.help.crypto.SymmetricCryptoAndroid +import io.legado.app.utils.MD5Utils + + +/** + * js加解密扩展类, 在js中通过java变量调用 + * 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md + */ +@Suppress("unused") +interface JsEncodeUtils { + + fun md5Encode(str: String): String { + return MD5Utils.md5Encode(str) + } + + fun md5Encode16(str: String): String { + return MD5Utils.md5Encode16(str) + } + + + //******************对称加密解密************************// + + /** + * 在js中这样使用 + * java.createSymmetricCrypto(transformation, key, iv).decrypt(data) + * java.createSymmetricCrypto(transformation, key, iv).decryptStr(data) + + * java.createSymmetricCrypto(transformation, key, iv).encrypt(data) + * java.createSymmetricCrypto(transformation, key, iv).encryptBase64(data) + * java.createSymmetricCrypto(transformation, key, iv).encryptHex(data) + */ + + /* 调用SymmetricCrypto key为null时使用随机密钥*/ + fun createSymmetricCrypto( + transformation: String, + key: ByteArray?, + iv: ByteArray? + ): SymmetricCrypto { + val symmetricCrypto = SymmetricCryptoAndroid(transformation, key) + return if (iv != null && iv.isNotEmpty()) symmetricCrypto.setIv(iv) else symmetricCrypto + } + + fun createSymmetricCrypto( + transformation: String, + key: ByteArray + ): SymmetricCrypto { + return createSymmetricCrypto(transformation, key, null) + } + + fun createSymmetricCrypto( + transformation: String, + key: String + ): SymmetricCrypto { + return createSymmetricCrypto(transformation, key, null) + } + + fun createSymmetricCrypto( + transformation: String, + key: String, + iv: String? + ): SymmetricCrypto { + return createSymmetricCrypto( + transformation, key.encodeToByteArray(), iv?.encodeToByteArray() + ) + } + //******************非对称加密解密************************// + + /* keys都为null时使用随机密钥 */ + fun createAsymmetricCrypto( + transformation: String + ): AsymmetricCrypto { + return AsymmetricCrypto(transformation) + } + + //******************签名************************// + fun createSign( + algorithm: String + ): Sign { + return Sign(algorithm) + } + //******************对称加密解密old************************// + + /////AES + /** + * AES 解码为 ByteArray + * @param str 传入的AES加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decrypt(str)") + ) + fun aesDecodeToByteArray( + str: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).decrypt(str) + } + + /** + * AES 解码为 String + * @param str 传入的AES加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(str)") + ) + fun aesDecodeToString( + str: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(str) + } + + /** + * AES解码为String,算法参数经过Base64加密 + * + * @param data 加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 解密后的字符串 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun aesDecodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto( + "AES/${mode}/${padding}", + Base64.decode(key, Base64.NO_WRAP), + Base64.decode(iv, Base64.NO_WRAP) + ).decryptStr(data) + } + + /** + * 已经base64的AES 解码为 ByteArray + * @param str 传入的AES Base64加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decrypt(str)") + ) + fun aesBase64DecodeToByteArray( + str: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).decrypt(str) + } + + /** + * 已经base64的AES 解码为 String + * @param str 传入的AES Base64加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(str)") + ) + fun aesBase64DecodeToString( + str: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(str) + } + + /** + * 加密aes为ByteArray + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decrypt(data)") + ) + fun aesEncodeToByteArray( + data: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).encrypt(data) + } + + /** + * 加密aes为String + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun aesEncodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(data) + } + + /** + * 加密aes后Base64化的ByteArray + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data).toByteArray()") + ) + fun aesEncodeToBase64ByteArray( + data: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).encryptBase64(data).toByteArray() + } + + /** + * 加密aes后Base64化的String + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun aesEncodeToBase64String( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).encryptBase64(data) + } + + + /** + * AES加密并转为Base64,算法参数经过Base64加密 + * + * @param data 被加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 加密后的Base64 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun aesEncodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto("AES/${mode}/${padding}", key, iv).encryptBase64(data) + } + + /////DES + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun desDecodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(data) + } + + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun desBase64DecodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(data) + } + + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encrypt(data)") + ) + fun desEncodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return String(createSymmetricCrypto(transformation, key, iv).encrypt(data)) + } + + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun desEncodeToBase64String( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).encryptBase64(data) + } + + //////3DES + /** + * 3DES解密 + * + * @param data 加密的字符串 + * @param key 密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv 加盐 + * @return 解密后的字符串 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun tripleDESDecodeStr( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto("DESede/${mode}/${padding}", key, iv).decryptStr(data) + } + + /** + * 3DES解密,算法参数经过Base64加密 + * + * @param data 加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 解密后的字符串 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun tripleDESDecodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto( + "DESede/${mode}/${padding}", + Base64.decode(key, Base64.NO_WRAP), + iv.encodeToByteArray() + ).decryptStr(data) + } + + + /** + * 3DES加密并转为Base64 + * + * @param data 被加密的字符串 + * @param key 密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv 加盐 + * @return 加密后的Base64 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun tripleDESEncodeBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto("DESede/${mode}/${padding}", key, iv) + .encryptBase64(data) + } + + /** + * 3DES加密并转为Base64,算法参数经过Base64加密 + * + * @param data 被加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 加密后的Base64 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun tripleDESEncodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto( + "DESede/${mode}/${padding}", + Base64.decode(key, Base64.NO_WRAP), + iv.encodeToByteArray() + ).encryptBase64(data) + } + +//******************消息摘要/散列消息鉴别码************************// + + /** + * 生成摘要,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @return 16进制字符串 + */ + fun digestHex( + data: String, + algorithm: String, + ): String { + return DigestUtil.digester(algorithm).digestHex(data) + } + + /** + * 生成摘要,并转为Base64字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @return Base64字符串 + */ + fun digestBase64Str( + data: String, + algorithm: String, + ): String { + return Base64.encodeToString(DigestUtil.digester(algorithm).digest(data), Base64.NO_WRAP) + } + + /** + * 生成散列消息鉴别码,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @param key 密钥 + * @return 16进制字符串 + */ + @Suppress("FunctionName") + fun HMacHex( + data: String, + algorithm: String, + key: String + ): String { + return HMac(algorithm, key.toByteArray()).digestHex(data) + } + + /** + * 生成散列消息鉴别码,并转为Base64字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @param key 密钥 + * @return Base64字符串 + */ + @Suppress("FunctionName") + fun HMacBase64( + data: String, + algorithm: String, + key: String + ): String { + return Base64.encodeToString( + HMac(algorithm, key.toByteArray()).digest(data), + Base64.NO_WRAP + ) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt new file mode 100755 index 000000000..082003c2f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -0,0 +1,1003 @@ +package io.legado.app.help + +import android.webkit.WebSettings +import androidx.annotation.Keep +import cn.hutool.core.codec.Base64 +import cn.hutool.core.util.HexUtil +import com.script.rhino.rhinoContext +import com.script.rhino.rhinoContextOrNull +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.dateFormat +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.BaseSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.http.BackstageWebView +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.help.http.CookieStore +import io.legado.app.help.http.SSLHelper +import io.legado.app.help.http.StrResponse +import io.legado.app.help.source.SourceVerificationHelp +import io.legado.app.help.source.getSourceType +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.QueryTTF +import io.legado.app.ui.association.OpenUrlConfirmActivity +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.ChineseUtils +import io.legado.app.utils.EncoderUtils +import io.legado.app.utils.EncodingDetect +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.JsURL +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.StringUtils +import io.legado.app.utils.UrlUtil +import io.legado.app.utils.compress.LibArchiveUtils +import io.legado.app.utils.createFileReplace +import io.legado.app.utils.externalCache +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isMainThread +import io.legado.app.utils.longToastOnUi +import io.legado.app.utils.mapAsync +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.startActivity +import io.legado.app.utils.toStringArray +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import okio.use +import org.jsoup.Connection +import org.jsoup.Jsoup +import splitties.init.appCtx +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.net.URLEncoder +import java.nio.charset.Charset +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.SimpleTimeZone +import java.util.UUID +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * js扩展类, 在js中通过java变量调用 + * 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md + * 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件 + * /android/data/{package}/cache/... + */ +@Keep +@Suppress("unused") +interface JsExtensions : JsEncodeUtils { + + fun getSource(): BaseSource? + + private val context: CoroutineContext + get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext + + /** + * 访问网络,返回String + */ + fun ajax(url: Any): String? { + val urlStr = if (url is List<*>) { + url.firstOrNull().toString() + } else { + url.toString() + } + val analyzeUrl = AnalyzeUrl(urlStr, source = getSource(), coroutineContext = context) + return kotlin.runCatching { + analyzeUrl.getStrResponse().body + }.onFailure { + rhinoContext.ensureActive() + AppLog.put("ajax(${urlStr}) error\n${it.localizedMessage}", it) + }.getOrElse { + it.stackTraceStr + } + } + + /** + * 并发访问网络 + */ + fun ajaxAll(urlList: Array): Array { + return runBlocking(context) { + urlList.asFlow().mapAsync(AppConfig.threadCount) { url -> + val analyzeUrl = AnalyzeUrl( + url, + source = getSource(), + coroutineContext = coroutineContext + ) + analyzeUrl.getStrResponseAwait() + }.flowOn(IO).toList().toTypedArray() + } + } + + /** + * 访问网络,返回Response + */ + fun connect(urlStr: String): StrResponse { + val analyzeUrl = AnalyzeUrl( + urlStr, + source = getSource(), + coroutineContext = context + ) + return kotlin.runCatching { + analyzeUrl.getStrResponse() + }.onFailure { + rhinoContext.ensureActive() + AppLog.put("connect(${urlStr}) error\n${it.localizedMessage}", it) + }.getOrElse { + StrResponse(analyzeUrl.url, it.stackTraceStr) + } + } + + fun connect(urlStr: String, header: String?): StrResponse { + val headerMap = GSON.fromJsonObject>(header).getOrNull() + val analyzeUrl = AnalyzeUrl( + urlStr, + headerMapF = headerMap, + source = getSource(), + coroutineContext = context + ) + return kotlin.runCatching { + analyzeUrl.getStrResponse() + }.onFailure { + rhinoContext.ensureActive() + AppLog.put("ajax($urlStr,$header) error\n${it.localizedMessage}", it) + }.getOrElse { + StrResponse(analyzeUrl.url, it.stackTraceStr) + } + } + + /** + * 使用webView访问网络 + * @param html 直接用webView载入的html, 如果html为空直接访问url + * @param url html内如果有相对路径的资源不传入url访问不了 + * @param js 用来取返回值的js语句, 没有就返回整个源代码 + * @return 返回js获取的内容 + */ + fun webView(html: String?, url: String?, js: String?): String? { + if (isMainThread) { + error("webView must be called on a background thread") + } + return runBlocking(context) { + BackstageWebView( + url = url, + html = html, + javaScript = js, + headerMap = getSource()?.getHeaderMap(true), + tag = getSource()?.getKey() + ).getStrResponse().body + } + } + + /** + * 使用webView获取资源url + */ + fun webViewGetSource(html: String?, url: String?, js: String?, sourceRegex: String): String? { + if (isMainThread) { + error("webViewGetSource must be called on a background thread") + } + return runBlocking(context) { + BackstageWebView( + url = url, + html = html, + javaScript = js, + headerMap = getSource()?.getHeaderMap(true), + tag = getSource()?.getKey(), + sourceRegex = sourceRegex + ).getStrResponse().body + } + } + + /** + * 使用webView获取跳转url + */ + fun webViewGetOverrideUrl( + html: String?, + url: String?, + js: String?, + overrideUrlRegex: String + ): String? { + if (isMainThread) { + error("webViewGetOverrideUrl must be called on a background thread") + } + return runBlocking(context) { + BackstageWebView( + url = url, + html = html, + javaScript = js, + headerMap = getSource()?.getHeaderMap(true), + tag = getSource()?.getKey(), + overrideUrlRegex = overrideUrlRegex + ).getStrResponse().body + } + } + + /** + * 使用内置浏览器打开链接,手动验证网站防爬 + * @param url 要打开的链接 + * @param title 浏览器页面的标题 + */ + fun startBrowser(url: String, title: String) { + rhinoContext.ensureActive() + SourceVerificationHelp.startBrowser(getSource(), url, title) + } + + /** + * 使用内置浏览器打开链接,并等待网页结果 + */ + fun startBrowserAwait(url: String, title: String, refetchAfterSuccess: Boolean): StrResponse { + rhinoContext.ensureActive() + val body = SourceVerificationHelp.getVerificationResult( + getSource(), url, title, true, refetchAfterSuccess + ) + return StrResponse(url, body) + } + + fun startBrowserAwait(url: String, title: String): StrResponse { + return startBrowserAwait(url, title, true) + } + + /** + * 打开图片验证码对话框,等待返回验证结果 + */ + fun getVerificationCode(imageUrl: String): String { + rhinoContext.ensureActive() + return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false) + } + + /** + * 可从网络,本地文件(阅读私有数据目录相对路径)导入JavaScript脚本 + */ + fun importScript(path: String): String { + val result = when { + path.startsWith("http") -> cacheFile(path) + else -> readTxtFile(path) + } + if (result.isBlank()) throw NoStackTraceException("$path 内容获取失败或者为空") + return result + } + + /** + * 缓存以文本方式保存的文件 如.js .txt等 + * @param urlStr 网络文件的链接 + * @return 返回缓存后的文件内容 + */ + fun cacheFile(urlStr: String): String { + return cacheFile(urlStr, 0) + } + + /** + * 缓存以文本方式保存的文件 如.js .txt等 + * @param saveTime 缓存时间,单位:秒 + */ + fun cacheFile(urlStr: String, saveTime: Int): String { + val key = md5Encode16(urlStr) + val cachePath = CacheManager.get(key) + return if ( + cachePath.isNullOrBlank() || + !getFile(cachePath).exists() + ) { + val path = downloadFile(urlStr) + log("首次下载 $urlStr >> $path") + CacheManager.put(key, path, saveTime) + readTxtFile(path) + } else { + readTxtFile(cachePath) + } + } + + /** + *js实现读取cookie + */ + fun getCookie(tag: String): String { + return getCookie(tag, null) + } + + fun getCookie(tag: String, key: String?): String { + return if (key != null) { + CookieStore.getKey(tag, key) + } else { + CookieStore.getCookie(tag) + } + } + + /** + * 下载文件 + * @param url 下载地址:可带参数type + * @return 下载的文件相对路径 + */ + fun downloadFile(url: String): String { + rhinoContext.ensureActive() + val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context) + val type = analyzeUrl.type ?: UrlUtil.getSuffix(url) + val path = FileUtils.getPath( + File(FileUtils.getCachePath()), + "${MD5Utils.md5Encode16(url)}.${type}" + ) + val file = File(path) + file.delete() + analyzeUrl.getInputStream().use { iStream -> + file.createFileReplace() + try { + file.outputStream().buffered().use { oStream -> + iStream.copyTo(oStream) + } + } catch (e: Throwable) { + file.delete() + throw e + } + } + return path.substring(FileUtils.getCachePath().length) + } + + + /** + * 实现16进制字符串转文件 + * @param content 需要转成文件的16进制字符串 + * @param url 通过url里的参数来判断文件类型 + * @return 相对路径 + */ + @Deprecated( + "Deprecated", + ReplaceWith("downloadFile(url)") + ) + fun downloadFile(content: String, url: String): String { + rhinoContext.ensureActive() + val type = AnalyzeUrl(url, source = getSource(), coroutineContext = context).type + ?: return "" + val path = FileUtils.getPath( + FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), + "${MD5Utils.md5Encode16(url)}.${type}" + ) + val file = File(path) + file.createFileReplace() + HexUtil.decodeHex(content).let { + if (it.isNotEmpty()) { + file.writeBytes(it) + } + } + return path.substring(FileUtils.getCachePath().length) + } + + /** + * js实现重定向拦截,网络访问get + */ + fun get(urlStr: String, headers: Map): Connection.Response { + val requestHeaders = if (getSource()?.enabledCookieJar == true) { + headers.toMutableMap().apply { put(cookieJarHeader, "1") } + } else headers + val rateLimiter = ConcurrentRateLimiter(getSource()) + val response = rateLimiter.withLimitBlocking { + rhinoContext.ensureActive() + Jsoup.connect(urlStr) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) + .ignoreContentType(true) + .followRedirects(false) + .headers(requestHeaders) + .method(Connection.Method.GET) + .execute() + } + return response + } + + /** + * js实现重定向拦截,网络访问head,不返回Response Body更省流量 + */ + fun head(urlStr: String, headers: Map): Connection.Response { + val requestHeaders = if (getSource()?.enabledCookieJar == true) { + headers.toMutableMap().apply { put(cookieJarHeader, "1") } + } else headers + val rateLimiter = ConcurrentRateLimiter(getSource()) + val response = rateLimiter.withLimitBlocking { + rhinoContext.ensureActive() + Jsoup.connect(urlStr) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) + .ignoreContentType(true) + .followRedirects(false) + .headers(requestHeaders) + .method(Connection.Method.HEAD) + .execute() + } + return response + } + + /** + * 网络访问post + */ + fun post(urlStr: String, body: String, headers: Map): Connection.Response { + val requestHeaders = if (getSource()?.enabledCookieJar == true) { + headers.toMutableMap().apply { put(cookieJarHeader, "1") } + } else headers + val rateLimiter = ConcurrentRateLimiter(getSource()) + val response = rateLimiter.withLimitBlocking { + rhinoContext.ensureActive() + Jsoup.connect(urlStr) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) + .ignoreContentType(true) + .followRedirects(false) + .requestBody(body) + .headers(requestHeaders) + .method(Connection.Method.POST) + .execute() + } + return response + } + + /* Str转ByteArray */ + fun strToBytes(str: String): ByteArray { + return str.toByteArray(charset("UTF-8")) + } + + fun strToBytes(str: String, charset: String): ByteArray { + return str.toByteArray(charset(charset)) + } + + /* ByteArray转Str */ + fun bytesToStr(bytes: ByteArray): String { + return String(bytes, charset("UTF-8")) + } + + fun bytesToStr(bytes: ByteArray, charset: String): String { + return String(bytes, charset(charset)) + } + + /** + * js实现base64解码,不能删 + */ + fun base64Decode(str: String?): String { + return Base64.decodeStr(str) + } + + fun base64Decode(str: String?, charset: String): String { + return Base64.decodeStr(str, charset(charset)) + } + + fun base64Decode(str: String, flags: Int): String { + return EncoderUtils.base64Decode(str, flags) + } + + fun base64DecodeToByteArray(str: String?): ByteArray? { + if (str.isNullOrBlank()) { + return null + } + return EncoderUtils.base64DecodeToByteArray(str, 0) + } + + fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? { + if (str.isNullOrBlank()) { + return null + } + return EncoderUtils.base64DecodeToByteArray(str, flags) + } + + fun base64Encode(str: String): String? { + return EncoderUtils.base64Encode(str, 2) + } + + fun base64Encode(str: String, flags: Int): String? { + return EncoderUtils.base64Encode(str, flags) + } + + /* HexString 解码为字节数组 */ + fun hexDecodeToByteArray(hex: String): ByteArray? { + return HexUtil.decodeHex(hex) + } + + /* hexString 解码为utf8String*/ + fun hexDecodeToString(hex: String): String? { + return HexUtil.decodeHexStr(hex) + } + + /* utf8 编码为hexString */ + fun hexEncodeToString(utf8: String): String? { + return HexUtil.encodeHexStr(utf8) + } + + /** + * 格式化时间 + */ + fun timeFormatUTC(time: Long, format: String, sh: Int): String? { + val utc = SimpleTimeZone(sh, "UTC") + return SimpleDateFormat(format, Locale.getDefault()).run { + timeZone = utc + format(Date(time)) + } + } + + /** + * 时间格式化 + */ + fun timeFormat(time: Long): String { + return dateFormat.format(Date(time)) + } + + fun encodeURI(str: String): String { + return try { + URLEncoder.encode(str, "UTF-8") + } catch (e: Exception) { + "" + } + } + + fun encodeURI(str: String, enc: String): String { + return try { + URLEncoder.encode(str, enc) + } catch (e: Exception) { + "" + } + } + + fun htmlFormat(str: String): String { + return HtmlFormatter.formatKeepImg(str) + } + + fun t2s(text: String): String { + return ChineseUtils.t2s(text) + } + + fun s2t(text: String): String { + return ChineseUtils.s2t(text) + } + + fun getWebViewUA(): String { + return WebSettings.getDefaultUserAgent(appCtx) + } + +//****************文件操作******************// + + /** + * 获取本地文件 + * @param path 相对路径 + * @return File + */ + fun getFile(path: String): File { + val cachePath = appCtx.externalCache.absolutePath + val aPath = if (path.startsWith(File.separator)) { + cachePath + path + } else { + cachePath + File.separator + path + } + val file = File(aPath) + val safePath = appCtx.externalCache.parent!! + if (!file.canonicalPath.startsWith(safePath)) { + throw SecurityException("非法路径") + } + return file + } + + fun readFile(path: String): ByteArray? { + val file = getFile(path) + if (file.exists()) { + return file.readBytes() + } + return null + } + + fun readTxtFile(path: String): String { + val file = getFile(path) + if (file.exists()) { + val charsetName = EncodingDetect.getEncode(file) + return String(file.readBytes(), charset(charsetName)) + } + return "" + } + + fun readTxtFile(path: String, charsetName: String): String { + val file = getFile(path) + if (file.exists()) { + return String(file.readBytes(), charset(charsetName)) + } + return "" + } + + /** + * 删除本地文件 + */ + fun deleteFile(path: String): Boolean { + val file = getFile(path) + return FileUtils.delete(file, true) + } + + /** + * js实现Zip压缩文件解压 + * @param zipPath 相对路径 + * @return 相对路径 + */ + fun unzipFile(zipPath: String): String { + return unArchiveFile(zipPath) + } + + /** + * js实现7Zip压缩文件解压 + * @param zipPath 相对路径 + * @return 相对路径 + */ + fun un7zFile(zipPath: String): String { + return unArchiveFile(zipPath) + } + + /** + * js实现Rar压缩文件解压 + * @param zipPath 相对路径 + * @return 相对路径 + */ + fun unrarFile(zipPath: String): String { + return unArchiveFile(zipPath) + } + + /** + * js实现压缩文件解压 + * @param zipPath 相对路径 + * @return 相对路径 + */ + fun unArchiveFile(zipPath: String): String { + if (zipPath.isEmpty()) return "" + val zipFile = getFile(zipPath) + return ArchiveUtils.deCompress(zipFile.absolutePath).let { + ArchiveUtils.TEMP_FOLDER_NAME + File.separator + MD5Utils.md5Encode16(zipFile.name) + } + } + + /** + * js实现文件夹内所有文本文件读取 + * @param path 文件夹相对路径 + * @return 所有文件字符串换行连接 + */ + fun getTxtInFolder(path: String): String { + if (path.isEmpty()) return "" + val folder = getFile(path) + val contents = StringBuilder() + folder.listFiles().let { + if (it != null) { + for (f in it) { + val charsetName = EncodingDetect.getEncode(f) + contents.append(String(f.readBytes(), charset(charsetName))) + .append("\n") + } + contents.deleteCharAt(contents.length - 1) + } + } + FileUtils.delete(folder.absolutePath) + return contents.toString() + } + + /** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接或十六进制字符串 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ + fun getZipStringContent(url: String, path: String): String { + val byteArray = getZipByteArrayContent(url, path) ?: return "" + val charsetName = EncodingDetect.getEncode(byteArray) + return String(byteArray, Charset.forName(charsetName)) + } + + fun getZipStringContent(url: String, path: String, charsetName: String): String { + val byteArray = getZipByteArrayContent(url, path) ?: return "" + return String(byteArray, Charset.forName(charsetName)) + } + + /** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接或十六进制字符串 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ + fun getRarStringContent(url: String, path: String): String { + val byteArray = getRarByteArrayContent(url, path) ?: return "" + val charsetName = EncodingDetect.getEncode(byteArray) + return String(byteArray, Charset.forName(charsetName)) + } + + fun getRarStringContent(url: String, path: String, charsetName: String): String { + val byteArray = getRarByteArrayContent(url, path) ?: return "" + return String(byteArray, Charset.forName(charsetName)) + } + + /** + * 获取网络7zip文件里面的数据 + * @param url 7zip文件的链接或十六进制字符串 + * @param path 所需获取文件在7zip内的路径 + * @return zip指定文件的数据 + */ + fun get7zStringContent(url: String, path: String): String { + val byteArray = get7zByteArrayContent(url, path) ?: return "" + val charsetName = EncodingDetect.getEncode(byteArray) + return String(byteArray, Charset.forName(charsetName)) + } + + fun get7zStringContent(url: String, path: String, charsetName: String): String { + val byteArray = get7zByteArrayContent(url, path) ?: return "" + return String(byteArray, Charset.forName(charsetName)) + } + + /** + * 获取网络zip文件里面的数据 + * @param url zip文件的链接或十六进制字符串 + * @param path 所需获取文件在zip内的路径 + * @return zip指定文件的数据 + */ + fun getZipByteArrayContent(url: String, path: String): ByteArray? { + val bytes = if (url.isAbsUrl()) { + AnalyzeUrl(url, source = getSource(), coroutineContext = context).getByteArray() + } else { + HexUtil.decodeHex(url) + } + val bos = ByteArrayOutputStream() + ZipInputStream(ByteArrayInputStream(bytes)).use { zis -> + var entry: ZipEntry + while (zis.nextEntry.also { entry = it } != null) { + if (entry.name.equals(path)) { + zis.use { it.copyTo(bos) } + return bos.toByteArray() + } + entry = zis.nextEntry + } + } + + log("getZipContent 未发现内容") + return null + } + + /** + * 获取网络Rar文件里面的数据 + * @param url Rar文件的链接或十六进制字符串 + * @param path 所需获取文件在Rar内的路径 + * @return Rar指定文件的数据 + */ + fun getRarByteArrayContent(url: String, path: String): ByteArray? { + val bytes = if (url.isAbsUrl()) { + AnalyzeUrl(url, source = getSource(), coroutineContext = context).getByteArray() + } else { + HexUtil.decodeHex(url) + } + + return ByteArrayInputStream(bytes).use { + LibArchiveUtils.getByteArrayContent(it, path) + } + } + + /** + * 获取网络7zip文件里面的数据 + * @param url 7zip文件的链接或十六进制字符串 + * @param path 所需获取文件在7zip内的路径 + * @return 7zip指定文件的数据 + */ + fun get7zByteArrayContent(url: String, path: String): ByteArray? { + val bytes = if (url.isAbsUrl()) { + AnalyzeUrl(url, source = getSource(), coroutineContext = context).getByteArray() + } else { + HexUtil.decodeHex(url) + } + + return ByteArrayInputStream(bytes).use { + LibArchiveUtils.getByteArrayContent(it, path) + } + } + + +//******************文件操作************************// + + /** + * 解析字体Base64数据,返回字体解析类 + */ + @Deprecated( + "Deprecated", + ReplaceWith("queryTTF(data)") + ) + fun queryBase64TTF(data: String?): QueryTTF? { + log("queryBase64TTF(String)方法已过时,并将在未来删除;请无脑使用queryTTF(Any)替代,新方法支持传入 url、本地文件、base64、ByteArray 自动判断&自动缓存,特殊情况需禁用缓存请传入第二可选参数false:Boolean") + return queryTTF(data) + } + + /** + * 返回字体解析类 + * @param data 支持url,本地文件,base64,ByteArray,自动判断,自动缓存 + * @param useCache 可选开关缓存,不传入该值默认开启缓存 + */ + @OptIn(ExperimentalStdlibApi::class) + fun queryTTF(data: Any?, useCache: Boolean): QueryTTF? { + try { + var key: String? = null + var qTTF: QueryTTF? + when (data) { + is String -> { + if (useCache) { + key = MessageDigest.getInstance("SHA-256").digest(data.toByteArray()) + .toHexString() + qTTF = AppCacheManager.getQueryTTF(key) + if (qTTF != null) return qTTF + } + val font: ByteArray? = when { + data.isAbsUrl() -> AnalyzeUrl( + data, + source = getSource(), + coroutineContext = context + ).getByteArray() + + else -> base64DecodeToByteArray(data) + } + font ?: return null + qTTF = QueryTTF(font) + } + + is ByteArray -> { + if (useCache) { + key = MessageDigest.getInstance("SHA-256").digest(data).toHexString() + qTTF = AppCacheManager.getQueryTTF(key) + if (qTTF != null) return qTTF + } + qTTF = QueryTTF(data) + } + + else -> return null + } + if (key != null) AppCacheManager.put(key, qTTF) + return qTTF + } catch (e: Exception) { + AppLog.put("[queryTTF] 获取字体处理类出错", e) + throw e + } + } + + fun queryTTF(data: Any?): QueryTTF? { + return queryTTF(data, true) + } + + /** + * @param text 包含错误字体的内容 + * @param errorQueryTTF 错误的字体 + * @param correctQueryTTF 正确的字体 + * @param filter 删除 errorQueryTTF 中不存在的字符 + */ + fun replaceFont( + text: String, + errorQueryTTF: QueryTTF?, + correctQueryTTF: QueryTTF?, + filter: Boolean + ): String { + if (errorQueryTTF == null || correctQueryTTF == null) return text + val contentArray = text.toStringArray() //这里不能用toCharArray,因为有些文字占多个字节 + val intArray = IntArray(1) + contentArray.forEachIndexed { index, s -> + val oldCode = s.codePointAt(0) + // 忽略正常的空白字符 + if (errorQueryTTF.isBlankUnicode(oldCode)) { + return@forEachIndexed + } + // 删除轮廓数据不存在的字符 + var glyf = errorQueryTTF.getGlyfByUnicode(oldCode) // 轮廓数据不存在 + if (errorQueryTTF.getGlyfIdByUnicode(oldCode) == 0) glyf = null // 轮廓数据指向保留索引0 + if (filter && (glyf == null)) { + contentArray[index] = "" + return@forEachIndexed + } + // 使用轮廓数据反查Unicode + val code = correctQueryTTF.getUnicodeByGlyf(glyf) + if (code != 0) { + intArray[0] = code + contentArray[index] = String(intArray, 0, 1) + } + } + return contentArray.joinToString("") + } + + /** + * @param text 包含错误字体的内容 + * @param errorQueryTTF 错误的字体 + * @param correctQueryTTF 正确的字体 + */ + fun replaceFont( + text: String, + errorQueryTTF: QueryTTF?, + correctQueryTTF: QueryTTF? + ): String { + return replaceFont(text, errorQueryTTF, correctQueryTTF, false) + } + + + /** + * 章节数转数字 + */ + fun toNumChapter(s: String?): String? { + s ?: return null + val matcher = AppPattern.titleNumPattern.matcher(s) + if (matcher.find()) { + val intStr = StringUtils.stringToInt(matcher.group(2)) + return "${matcher.group(1)}${intStr}${matcher.group(3)}" + } + return s + } + + + fun toURL(urlStr: String): JsURL { + return JsURL(urlStr) + } + + fun toURL(url: String, baseUrl: String? = null): JsURL { + return JsURL(url, baseUrl) + } + + /** + * 弹窗提示 + */ + fun toast(msg: Any?) { + rhinoContext.ensureActive() + appCtx.toastOnUi("${getSource()?.getTag()}: ${msg.toString()}") + } + + /** + * 弹窗提示 停留时间较长 + */ + fun longToast(msg: Any?) { + rhinoContext.ensureActive() + appCtx.longToastOnUi("${getSource()?.getTag()}: ${msg.toString()}") + } + + /** + * 输出调试日志 + */ + fun log(msg: Any?): Any? { + rhinoContextOrNull?.ensureActive() + getSource()?.let { + Debug.log(it.getKey(), msg.toString()) + } ?: Debug.log(msg.toString()) + AppLog.putDebug("${getSource()?.getTag() ?: "源"}调试输出: $msg") + return msg + } + + /** + * 输出对象类型 + */ + fun logType(any: Any?) { + if (any == null) { + log("null") + } else { + log(any.javaClass.name) + } + } + + /** + * 生成UUID + */ + fun randomUUID(): String { + return UUID.randomUUID().toString() + } + + fun androidId(): String { + return AppConst.androidId + } + + fun openUrl(url: String) { + openUrl(url, null) + } + + // 新增 mimeType 参数,默认为 null(保持兼容性) + fun openUrl(url: String, mimeType: String? = null) { + require(url.length < 64 * 1024) { "openUrl parameter url too long" } + rhinoContext.ensureActive() + val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null") + appCtx.startActivity { + putExtra("uri", url) + putExtra("mimeType", mimeType) + putExtra("sourceOrigin", source.getKey()) + putExtra("sourceName", source.getTag()) + putExtra("sourceType", source.getSourceType()) + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt b/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt new file mode 100755 index 000000000..3a226e071 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt @@ -0,0 +1,65 @@ +package io.legado.app.help + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.os.Build +import io.legado.app.R +import io.legado.app.ui.welcome.* +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +/** + * Created by GKF on 2018/2/27. + * 更换图标 + */ +object LauncherIconHelp { + private val packageManager: PackageManager = appCtx.packageManager + private val componentNames = arrayListOf( + ComponentName(appCtx, Launcher1::class.java.name), + ComponentName(appCtx, Launcher2::class.java.name), + ComponentName(appCtx, Launcher3::class.java.name), + ComponentName(appCtx, Launcher4::class.java.name), + ComponentName(appCtx, Launcher5::class.java.name), + ComponentName(appCtx, Launcher6::class.java.name) + ) + + fun changeIcon(icon: String?) { + if (icon.isNullOrEmpty()) return + if (Build.VERSION.SDK_INT < 26) { + appCtx.toastOnUi(R.string.change_icon_error) + return + } + var hasEnabled = false + componentNames.forEach { + if (icon.equals(it.className.substringAfterLast("."), true)) { + hasEnabled = true + //启用 + packageManager.setComponentEnabledSetting( + it, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + } else { + //禁用 + packageManager.setComponentEnabledSetting( + it, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } + if (hasEnabled) { + packageManager.setComponentEnabledSetting( + ComponentName(appCtx, WelcomeActivity::class.java.name), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } else { + packageManager.setComponentEnabledSetting( + ComponentName(appCtx, WelcomeActivity::class.java.name), + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/LayoutManager.kt b/app/src/main/java/io/legado/app/help/LayoutManager.kt new file mode 100755 index 000000000..4dee2ab06 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/LayoutManager.kt @@ -0,0 +1,73 @@ +package io.legado.app.help + +import androidx.annotation.IntDef +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager + +@Suppress("unused") +object LayoutManager { + + interface LayoutManagerFactory { + fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager + } + + @IntDef(LinearLayoutManager.HORIZONTAL, LinearLayoutManager.VERTICAL) + @Retention(AnnotationRetention.SOURCE) + annotation class Orientation + + fun linear(): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return LinearLayoutManager(recyclerView.context) + } + } + } + + + fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return LinearLayoutManager(recyclerView.context, orientation, reverseLayout) + } + } + } + + + fun grid(spanCount: Int): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return GridLayoutManager(recyclerView.context, spanCount) + } + } + } + + + fun grid( + spanCount: Int, + @Orientation orientation: Int, + reverseLayout: Boolean + ): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return GridLayoutManager( + recyclerView.context, + spanCount, + orientation, + reverseLayout + ) + } + } + } + + + fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return StaggeredGridLayoutManager(spanCount, orientation) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/LifecycleHelp.kt b/app/src/main/java/io/legado/app/help/LifecycleHelp.kt new file mode 100755 index 000000000..f05cb6567 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/LifecycleHelp.kt @@ -0,0 +1,121 @@ +package io.legado.app.help + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import io.legado.app.base.BaseService +import io.legado.app.utils.LogUtils +import java.lang.ref.WeakReference + +/** + * Activity管理器,管理项目中Activity的状态 + */ +@Suppress("unused") +object LifecycleHelp : Application.ActivityLifecycleCallbacks { + + private const val TAG = "LifecycleHelp" + + private val activities: MutableList> = arrayListOf() + private val services: MutableList> = arrayListOf() + private var appFinishedListener: (() -> Unit)? = null + + fun activitySize(): Int { + return activities.size + } + + /** + * 判断指定Activity是否存在 + */ + fun isExistActivity(activityClass: Class<*>): Boolean { + activities.forEach { item -> + if (item.get()?.javaClass == activityClass) { + return true + } + } + return false + } + + /** + * 关闭指定 activity(class) + */ + fun finishActivity(vararg activityClasses: Class<*>) { + val waitFinish = ArrayList>() + for (temp in activities) { + for (activityClass in activityClasses) { + if (temp.get()?.javaClass == activityClass) { + waitFinish.add(temp) + break + } + } + } + waitFinish.forEach { + it.get()?.finish() + } + } + + fun setOnAppFinishedListener(appFinishedListener: (() -> Unit)) { + this.appFinishedListener = appFinishedListener + } + + override fun onActivityPaused(activity: Activity) { + LogUtils.d(TAG, "${activity::class.simpleName} onPause") + } + + override fun onActivityResumed(activity: Activity) { + LogUtils.d(TAG, "${activity::class.simpleName} onResume") + } + + override fun onActivityStarted(activity: Activity) { + LogUtils.d(TAG, "${activity::class.simpleName} onStart") + } + + override fun onActivityDestroyed(activity: Activity) { + LogUtils.d(TAG, "${activity::class.simpleName} onDestroy") + for (temp in activities) { + if (temp.get() != null && temp.get() === activity) { + activities.remove(temp) + if (services.size == 0 && activities.size == 0) { + onAppFinished() + } + break + } + } + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + LogUtils.d(TAG, "${activity::class.simpleName} onSaveInstanceState") + } + + override fun onActivityStopped(activity: Activity) { + LogUtils.d(TAG, "${activity::class.simpleName} onStop") + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + LogUtils.d(TAG, "${activity::class.simpleName} onCreate") + activities.add(WeakReference(activity)) + } + + @Synchronized + fun onServiceCreate(service: BaseService) { + LogUtils.d(TAG, "${service::class.simpleName} onCreate") + services.add(WeakReference(service)) + } + + @Synchronized + fun onServiceDestroy(service: BaseService) { + LogUtils.d(TAG, "${service::class.simpleName} onDestroy") + for (temp in services) { + if (temp.get() != null && temp.get() === service) { + services.remove(temp) + if (services.size == 0 && activities.size == 0) { + onAppFinished() + } + break + } + } + } + + private fun onAppFinished() { + appFinishedListener?.invoke() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/MediaHelp.kt b/app/src/main/java/io/legado/app/help/MediaHelp.kt new file mode 100755 index 000000000..d0324646c --- /dev/null +++ b/app/src/main/java/io/legado/app/help/MediaHelp.kt @@ -0,0 +1,70 @@ +package io.legado.app.help + +import android.content.Context +import android.media.AudioManager +import android.media.MediaPlayer +import android.support.v4.media.session.PlaybackStateCompat +import androidx.media.AudioAttributesCompat +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat +import io.legado.app.R +import splitties.systemservices.audioManager + +object MediaHelp { + + const val MEDIA_SESSION_ACTIONS = (PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + or PlaybackStateCompat.ACTION_REWIND + or PlaybackStateCompat.ACTION_PLAY + or PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_PAUSE + or PlaybackStateCompat.ACTION_STOP + or PlaybackStateCompat.ACTION_FAST_FORWARD + or PlaybackStateCompat.ACTION_SKIP_TO_NEXT + or PlaybackStateCompat.ACTION_SEEK_TO + or PlaybackStateCompat.ACTION_SET_RATING + or PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + or PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + or PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM + or PlaybackStateCompat.ACTION_PLAY_FROM_URI + or PlaybackStateCompat.ACTION_PREPARE + or PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + or PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH + or PlaybackStateCompat.ACTION_PREPARE_FROM_URI + or PlaybackStateCompat.ACTION_SET_REPEAT_MODE + or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE + or PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED) + + fun buildAudioFocusRequestCompat( + audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener + ): AudioFocusRequestCompat { + val mPlaybackAttributes = AudioAttributesCompat.Builder() + .setUsage(AudioAttributesCompat.USAGE_MEDIA) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) + .build() + return AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setAudioAttributes(mPlaybackAttributes) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + } + + + /** + * @return 音频焦点 + */ + fun requestFocus(focusRequest: AudioFocusRequestCompat): Boolean { + val request = AudioManagerCompat.requestAudioFocus(audioManager, focusRequest) + return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + } + + /** + * 播放静音音频,用来获取音频焦点 + */ + fun playSilentSound(mContext: Context) { + kotlin.runCatching { + // Stupid Android 8 "Oreo" hack to make media buttons work + val mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent_sound) + mMediaPlayer.setOnCompletionListener { mMediaPlayer.release() } + mMediaPlayer.start() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/PaintPool.kt b/app/src/main/java/io/legado/app/help/PaintPool.kt new file mode 100755 index 000000000..953b5375b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/PaintPool.kt @@ -0,0 +1,17 @@ +package io.legado.app.help + +import android.graphics.Paint +import io.legado.app.utils.objectpool.BaseSafeObjectPool + +object PaintPool : BaseSafeObjectPool(8) { + + private val emptyPaint = Paint() + + override fun create(): Paint = Paint() + + override fun recycle(target: Paint) { + target.set(emptyPaint) + super.recycle(target) + } + +} diff --git a/app/src/main/java/io/legado/app/help/README.md b/app/src/main/java/io/legado/app/help/README.md new file mode 100755 index 000000000..9bf5306f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/README.md @@ -0,0 +1 @@ +# 放置一些帮助类 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ReplaceAnalyzer.kt b/app/src/main/java/io/legado/app/help/ReplaceAnalyzer.kt new file mode 100755 index 000000000..43004c0be --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ReplaceAnalyzer.kt @@ -0,0 +1,47 @@ +package io.legado.app.help + +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.utils.* + +object ReplaceAnalyzer { + + fun jsonToReplaceRules(json: String): Result> { + return kotlin.runCatching { + val replaceRules = mutableListOf() + val items: List> = jsonPath.parse(json).read("$") + for (item in items) { + val jsonItem = jsonPath.parse(item) + jsonToReplaceRule(jsonItem.jsonString()).getOrThrow().let { + if (it.isValid()) { + replaceRules.add(it) + } + } + } + replaceRules + } + } + + fun jsonToReplaceRule(json: String): Result { + return runCatching { + val replaceRule: ReplaceRule? = + GSON.fromJsonObject(json.trim()).getOrNull() + if (replaceRule == null || replaceRule.pattern.isEmpty()) { + val jsonItem = jsonPath.parse(json.trim()) + val rule = ReplaceRule() + rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis() + rule.pattern = jsonItem.readString("$.regex") ?: "" + if (rule.pattern.isEmpty()) throw NoStackTraceException("格式不对") + rule.name = jsonItem.readString("$.replaceSummary") ?: "" + rule.replacement = jsonItem.readString("$.replacement") ?: "" + rule.isRegex = jsonItem.readBool("$.isRegex") == true + rule.scope = jsonItem.readString("$.useTo") + rule.isEnabled = jsonItem.readBool("$.enable") == true + rule.order = jsonItem.readInt("$.serialNumber") ?: 0 + return@runCatching rule + } + return@runCatching replaceRule + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/RuleBigDataHelp.kt b/app/src/main/java/io/legado/app/help/RuleBigDataHelp.kt new file mode 100755 index 000000000..83c2feeae --- /dev/null +++ b/app/src/main/java/io/legado/app/help/RuleBigDataHelp.kt @@ -0,0 +1,146 @@ +package io.legado.app.help + +import io.legado.app.data.appDb +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getFile +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.io.File + +object RuleBigDataHelp { + + private val ruleDataDir = FileUtils.createFolderIfNotExist(appCtx.externalFiles, "ruleData") + private val bookData = FileUtils.createFolderIfNotExist(ruleDataDir, "book") + private val rssData = FileUtils.createFolderIfNotExist(ruleDataDir, "rss") + + suspend fun clearInvalid() { + withContext(IO) { + bookData.listFiles()?.forEach { + if (it.isFile) { + FileUtils.delete(it) + } else { + val bookUrlFile = it.getFile("bookUrl.txt") + if (!bookUrlFile.exists()) { + FileUtils.delete(it) + } else { + val bookUrl = bookUrlFile.readText() + if (!appDb.bookDao.has(bookUrl)) { + FileUtils.delete(it) + } + } + } + } + rssData.listFiles()?.forEach { + if (it.isFile) { + FileUtils.delete(it) + } else { + val originFile = it.getFile("origin.txt") + if (!originFile.exists()) { + FileUtils.delete(it) + } else { + val origin = originFile.readText() + if (!appDb.rssSourceDao.has(origin)) { + FileUtils.delete(it) + } + } + } + } + } + } + + fun putBookVariable(bookUrl: String, key: String, value: String?) { + val md5BookUrl = MD5Utils.md5Encode(bookUrl) + val md5Key = MD5Utils.md5Encode(key) + if (value == null) { + FileUtils.delete(FileUtils.getPath(bookData, md5BookUrl, "$md5Key.txt"), true) + } else { + val valueFile = FileUtils.createFileIfNotExist(bookData, md5BookUrl, "$md5Key.txt") + valueFile.writeText(value) + val bookUrlFile = File(FileUtils.getPath(bookData, md5BookUrl, "bookUrl.txt")) + if (!bookUrlFile.exists()) { + bookUrlFile.writeText(bookUrl) + } + } + } + + fun getBookVariable(bookUrl: String, key: String?): String? { + val md5BookUrl = MD5Utils.md5Encode(bookUrl) + val md5Key = MD5Utils.md5Encode(key) + val file = File(FileUtils.getPath(bookData, md5BookUrl, "$md5Key.txt")) + if (file.exists()) { + return file.readText() + } + return null + } + + fun hasBookVariable(bookUrl: String, key: String): Boolean { + val md5BookUrl = MD5Utils.md5Encode(bookUrl) + val md5Key = MD5Utils.md5Encode(key) + val file = File(FileUtils.getPath(bookData, md5BookUrl, "$md5Key.txt")) + return file.exists() + } + + fun putChapterVariable(bookUrl: String, chapterUrl: String, key: String, value: String?) { + val md5BookUrl = MD5Utils.md5Encode(bookUrl) + val md5ChapterUrl = MD5Utils.md5Encode(chapterUrl) + val md5Key = MD5Utils.md5Encode(key) + if (value == null) { + FileUtils.delete(FileUtils.getPath(bookData, md5BookUrl, md5ChapterUrl, "$md5Key.txt")) + } else { + val valueFile = + FileUtils.createFileIfNotExist(bookData, md5BookUrl, md5ChapterUrl, "$md5Key.txt") + valueFile.writeText(value) + val bookUrlFile = File(FileUtils.getPath(bookData, md5BookUrl, "bookUrl.txt")) + if (!bookUrlFile.exists()) { + bookUrlFile.writeText(bookUrl) + } + } + } + + fun getChapterVariable(bookUrl: String, chapterUrl: String, key: String): String? { + val md5BookUrl = MD5Utils.md5Encode(bookUrl) + val md5ChapterUrl = MD5Utils.md5Encode(chapterUrl) + val md5Key = MD5Utils.md5Encode(key) + val file = File(FileUtils.getPath(bookData, md5BookUrl, md5ChapterUrl, "$md5Key.txt")) + if (file.exists()) { + return file.readText() + } + return null + } + + fun putRssVariable(origin: String, link: String, key: String, value: String?) { + val md5Origin = MD5Utils.md5Encode(origin) + val md5Link = MD5Utils.md5Encode(link) + val md5Key = MD5Utils.md5Encode(key) + val filePath = FileUtils.getPath(rssData, md5Origin, md5Link, "$md5Key.txt") + if (value == null) { + FileUtils.delete(filePath) + } else { + val valueFile = FileUtils.createFileIfNotExist(filePath) + valueFile.writeText(value) + val originFile = File(FileUtils.getPath(rssData, md5Origin, "origin.txt")) + if (!originFile.exists()) { + originFile.writeText(origin) + } + val linFile = File(FileUtils.getPath(rssData, md5Origin, md5Link, "origin.txt")) + if (!linFile.exists()) { + linFile.writeText(link) + } + } + } + + fun getRssVariable(origin: String, link: String, key: String): String? { + val md5Origin = MD5Utils.md5Encode(origin) + val md5Link = MD5Utils.md5Encode(link) + val md5Key = MD5Utils.md5Encode(key) + val filePath = FileUtils.getPath(rssData, md5Origin, md5Link, "$md5Key.txt") + val file = File(filePath) + if (file.exists()) { + return file.readText() + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/RuleComplete.kt b/app/src/main/java/io/legado/app/help/RuleComplete.kt new file mode 100755 index 000000000..d37595e83 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/RuleComplete.kt @@ -0,0 +1,88 @@ +package io.legado.app.help + + +@Suppress("RegExpRedundantEscape") +object RuleComplete { + // 需要补全 + private val needComplete = Regex( + """(?\&{2}|%%|\|{2}|$)""" + ) + + // 不能补全 存在js/json/{{xx}}的复杂情况 + private val notComplete = Regex("""^:|^##|\{\{|@js:||@Json:|\$\.""") + + // 修正从图片获取信息 + private val fixImgInfo = + Regex("""(?<=(^|tag\.|[\+/@>~| &]))img(?(\[@?.+\]|\.[-\w]+)?)[@/]+text(\(\))?(?\&{2}|%%|\|{2}|$)""") + + private val isXpath = Regex("^//|^@Xpath:") + + /** + * 对简单规则进行补全,简化部分书源规则的编写 + * 对JSOUP/XPath/CSS规则生效 + * @author 希弥 + * @return 补全后的规则 或 原规则 + * @param rules 需要补全的规则 + * @param preRule 预处理规则或列表规则 + * @param type 补全结果的类型,可选的值有: + * 1 文字(默认) + * 2 链接 + * 3 图片 + */ + fun autoComplete( + rules: String?, + preRule: String? = null, + type: Int = 1 + ): String? { + if (rules.isNullOrEmpty() || rules.contains(notComplete) || preRule?.contains(notComplete) == true) { + return rules + } + + /** 尾部##分割的正则或由,分割的参数 */ + val tailStr: String + + /** 分割字符 */ + val splitStr: String + + /** 用于获取文字时添加的规则 */ + val textRule: String + + /** 用于获取链接时添加的规则 */ + val linkRule: String + + /** 用于获取图片时添加的规则 */ + val imgRule: String + + /** 用于获取图片alt属性时添加的规则 */ + val imgText: String + + // 分离尾部规则 + val regexSplit = rules.split("""##|,\{""".toRegex(), 2) + val cleanedRule = regexSplit[0] + if (regexSplit.size > 1) { + splitStr = """##|,\{""".toRegex().find(rules)?.value ?: "" + tailStr = splitStr + regexSplit[1] + } else { + tailStr = "" + } + if (cleanedRule.contains(isXpath)) { + textRule = "//text()\${seq}" + linkRule = "//@href\${seq}" + imgRule = "//@src\${seq}" + imgText = "img\${at}/@alt\${seq}" + } else { + textRule = "@text\${seq}" + linkRule = "@href\${seq}" + imgRule = "@src\${seq}" + imgText = "img\${at}@alt\${seq}" + } + return when (type) { + 1 -> needComplete.replace(cleanedRule, textRule).replace(fixImgInfo, imgText) + tailStr + 2 -> needComplete.replace(cleanedRule, linkRule) + tailStr + 3 -> needComplete.replace(cleanedRule, imgRule) + tailStr + else -> rules + } + } + + +} diff --git a/app/src/main/java/io/legado/app/help/TTS.kt b/app/src/main/java/io/legado/app/help/TTS.kt new file mode 100755 index 000000000..3165bb3e8 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/TTS.kt @@ -0,0 +1,145 @@ +package io.legado.app.help + +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +class TTS { + + private val handler by lazy { buildMainHandler() } + + private val tag = "legado_tts" + + private val clearTtsRunnable = Runnable { clearTts() } + + private var speakStateListener: SpeakStateListener? = null + + private var textToSpeech: TextToSpeech? = null + + private var text: String? = null + + private var onInit = false + + private val initListener by lazy { + InitListener() + } + + private val utteranceListener by lazy { + TTSUtteranceListener() + } + + val isSpeaking: Boolean + get() { + return textToSpeech?.isSpeaking ?: false + } + + @Suppress("unused") + fun setSpeakStateListener(speakStateListener: SpeakStateListener) { + this.speakStateListener = speakStateListener + } + + @Suppress("unused") + fun removeSpeakStateListener() { + speakStateListener = null + } + + @Synchronized + fun speak(text: String) { + handler.removeCallbacks(clearTtsRunnable) + this.text = text + if (onInit) { + return + } + if (textToSpeech == null) { + onInit = true + textToSpeech = TextToSpeech(appCtx, initListener) + } else { + addTextToSpeakList() + } + } + + fun stop() { + textToSpeech?.stop() + } + + @Synchronized + fun clearTts() { + textToSpeech?.let { tts -> + tts.stop() + tts.shutdown() + } + textToSpeech = null + } + + private fun addTextToSpeakList() { + val tts = textToSpeech ?: return + kotlin.runCatching { + var result = tts.speak("", TextToSpeech.QUEUE_FLUSH, null, null) + if (result == TextToSpeech.ERROR) { + clearTts() + textToSpeech = TextToSpeech(appCtx, initListener) + return + } + text?.splitNotBlank("\n")?.forEachIndexed { i, s -> + result = tts.speak(s, TextToSpeech.QUEUE_ADD, null, tag + i) + if (result == TextToSpeech.ERROR) { + AppLog.put("tts朗读出错:$text") + } + } + }.onFailure { + AppLog.put("tts朗读出错", it) + appCtx.toastOnUi(it.localizedMessage) + } + } + + /** + * 初始化监听 + */ + private inner class InitListener : TextToSpeech.OnInitListener { + + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + textToSpeech?.setOnUtteranceProgressListener(utteranceListener) + addTextToSpeakList() + } else { + appCtx.toastOnUi(R.string.tts_init_failed) + } + onInit = false + } + + } + + /** + * 朗读监听 + */ + private inner class TTSUtteranceListener : UtteranceProgressListener() { + + override fun onStart(utteranceId: String?) { + //开始朗读取消释放资源任务 + handler.removeCallbacks(clearTtsRunnable) + speakStateListener?.onStart() + } + + override fun onDone(utteranceId: String?) { + //一分钟没有朗读释放资源 + handler.postDelayed(clearTtsRunnable, 60000L) + speakStateListener?.onDone() + } + + @Deprecated("Deprecated in Java") + override fun onError(utteranceId: String?) { + //Deprecated + } + + } + + interface SpeakStateListener { + fun onStart() + fun onDone() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/book/BookContent.kt b/app/src/main/java/io/legado/app/help/book/BookContent.kt new file mode 100755 index 000000000..900eecc55 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/book/BookContent.kt @@ -0,0 +1,16 @@ +package io.legado.app.help.book + +import io.legado.app.data.entities.ReplaceRule + +data class BookContent( + val sameTitleRemoved: Boolean, + val textList: List, + //起效的替换规则 + val effectiveReplaceRules: List? +) { + + override fun toString(): String { + return textList.joinToString("\n") + } + +} diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt new file mode 100755 index 000000000..a2ac13e69 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt @@ -0,0 +1,382 @@ +@file:Suppress("unused") + +package io.legado.app.help.book + +import android.net.Uri +import androidx.core.net.toUri +import com.script.buildScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.BookSourceType +import io.legado.app.constant.BookType +import io.legado.app.data.appDb +import io.legado.app.data.entities.BaseBook +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.RuleBigDataHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.FileDoc +import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.exists +import io.legado.app.utils.find +import io.legado.app.utils.inputStream +import io.legado.app.utils.isUri +import io.legado.app.utils.normalizeFileName +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx +import java.io.File +import java.time.LocalDate +import java.time.Period.between +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max +import kotlin.math.min + + +val Book.isAudio: Boolean + get() = isType(BookType.audio) + +val Book.isImage: Boolean + get() = isType(BookType.image) + +val Book.isLocal: Boolean + get() { + if (type == 0) { + return origin == BookType.localTag || origin.startsWith(BookType.webDavTag) + } + return isType(BookType.local) + } + +val Book.isLocalTxt: Boolean + get() = isLocal && originName.endsWith(".txt", true) + +val Book.isEpub: Boolean + get() = isLocal && originName.endsWith(".epub", true) + +val Book.isUmd: Boolean + get() = isLocal && originName.endsWith(".umd", true) + +val Book.isPdf: Boolean + get() = isLocal && originName.endsWith(".pdf", true) + +val Book.isMobi: Boolean + get() = isLocal && (originName.endsWith(".mobi", true) || + originName.endsWith(".azw3", true) || + originName.endsWith(".azw", true)) + +val Book.isOnLineTxt: Boolean + get() = !isLocal && isType(BookType.text) + +val Book.isWebFile: Boolean + get() = isType(BookType.webFile) + +val Book.isUpError: Boolean + get() = isType(BookType.updateError) + +val Book.isArchive: Boolean + get() = isType(BookType.archive) + +val Book.isNotShelf: Boolean + get() = isType(BookType.notShelf) + +val Book.archiveName: String + get() { + if (!isArchive) throw NoStackTraceException("Book is not deCompressed from archive") + // local_book::archive.rar + // webDav::https://...../archive.rar + return origin.substringAfter("::").substringAfterLast("/") + } + +fun Book.contains(word: String?): Boolean { + if (word.isNullOrEmpty()) { + return true + } + return name.contains(word) + || author.contains(word) + || originName.contains(word) + || origin.contains(word) + || kind?.contains(word) == true + || intro?.contains(word) == true +} + +private val localUriCache by lazy { + ConcurrentHashMap() +} + +fun Book.getLocalUri(): Uri { + if (!isLocal) { + throw NoStackTraceException("不是本地书籍") + } + var uri = localUriCache[bookUrl] + if (uri != null) { + return uri + } + uri = if (bookUrl.isUri()) { + bookUrl.toUri() + } else { + Uri.fromFile(File(bookUrl)) + } + //先检测uri是否有效,这个比较快 + uri.inputStream(appCtx).getOrNull()?.use { + localUriCache[bookUrl] = uri + }?.let { + return uri + } + //不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件 + val defaultBookDir = AppConfig.defaultBookTreeUri + val importBookDir = AppConfig.importBookPath + + // 查找书籍保存目录 + if (!defaultBookDir.isNullOrBlank()) { + val treeUri = defaultBookDir.toUri() + val treeFileDoc = FileDoc.fromUri(treeUri, true) + if (!treeFileDoc.exists()) { + appCtx.toastOnUi("书籍保存目录失效,请重新设置!") + } else { + val fileDoc = treeFileDoc.find(originName, 5, 100) + if (fileDoc != null) { + localUriCache[bookUrl] = fileDoc.uri + //更新bookUrl 重启不用再找一遍 + bookUrl = fileDoc.toString() + save() + return fileDoc.uri + } + } + } + + // 查找添加本地选择的目录 + if (!importBookDir.isNullOrBlank() && defaultBookDir != importBookDir) { + val treeUri = if (importBookDir.isUri()) { + importBookDir.toUri() + } else { + Uri.fromFile(File(importBookDir)) + } + val treeFileDoc = FileDoc.fromUri(treeUri, true) + val fileDoc = treeFileDoc.find(originName, 5, 100) + if (fileDoc != null) { + localUriCache[bookUrl] = fileDoc.uri + bookUrl = fileDoc.toString() + save() + return fileDoc.uri + } + } + + localUriCache[bookUrl] = uri + return uri +} + + +fun Book.getArchiveUri(): Uri? { + val defaultBookDir = AppConfig.defaultBookTreeUri + return if (isArchive && !defaultBookDir.isNullOrBlank()) { + FileDoc.fromUri(defaultBookDir.toUri(), true) + .find(archiveName)?.uri + } else { + null + } +} + +fun Book.cacheLocalUri(uri: Uri) { + localUriCache[bookUrl] = uri +} + +fun Book.removeLocalUriCache() { + localUriCache.remove(bookUrl) +} + +fun Book.getRemoteUrl(): String? { + if (origin.startsWith(BookType.webDavTag)) { + return origin.substring(BookType.webDavTag.length) + } + return null +} + +fun Book.setType(@BookType.Type vararg types: Int) { + type = 0 + addType(*types) +} + +fun Book.addType(@BookType.Type vararg types: Int) { + types.forEach { + type = type or it + } +} + +fun Book.removeType(@BookType.Type vararg types: Int) { + types.forEach { + type = type and it.inv() + } +} + +fun Book.removeAllBookType() { + removeType(BookType.allBookType) +} + +fun Book.clearType() { + type = 0 +} + +fun Book.isType(@BookType.Type bookType: Int): Boolean = type and bookType > 0 + +fun Book.upType() { + if (type < 8) { + type = when (type) { + BookSourceType.image -> BookType.image + BookSourceType.audio -> BookType.audio + BookSourceType.file -> BookType.webFile + else -> BookType.text + } + if (origin == BookType.localTag || origin.startsWith(BookType.webDavTag)) { + type = type or BookType.local + } + } +} + +fun Book.sync(oldBook: Book) { + val curBook = appDb.bookDao.getBook(oldBook.bookUrl)!! + durChapterTime = curBook.durChapterTime + durChapterPos = curBook.durChapterPos + if (durChapterIndex != curBook.durChapterIndex) { + durChapterIndex = curBook.durChapterIndex + val replaceRules = ContentProcessor.get(this).getTitleReplaceRules() + appDb.bookChapterDao.getChapter(bookUrl, durChapterIndex)?.let { + durChapterTitle = it.getDisplayTitle(replaceRules, getUseReplaceRule()) + } + } + canUpdate = curBook.canUpdate + readConfig = curBook.readConfig +} + +fun Book.update() { + appDb.bookDao.update(this) +} + +fun Book.primaryStr(): String { + return origin + bookUrl +} + +fun Book.updateTo(newBook: Book): Book { + newBook.durChapterIndex = durChapterIndex + newBook.durChapterTitle = durChapterTitle + newBook.durChapterPos = durChapterPos + newBook.durChapterTime = durChapterTime + newBook.group = group + newBook.order = order + newBook.customCoverUrl = customCoverUrl + newBook.customIntro = customIntro + newBook.customTag = customTag + newBook.canUpdate = canUpdate + newBook.readConfig = readConfig + val variableMap = variableMap.toMutableMap() + variableMap.keys.removeIf { + newBook.hasVariable(it) + } + newBook.variableMap.putAll(variableMap) + newBook.variable = GSON.toJson(newBook.variableMap) + return newBook +} + +fun Book.hasVariable(key: String): Boolean { + return variableMap.contains(key) || RuleBigDataHelp.hasBookVariable(bookUrl, key) +} + +fun Book.getFolderNameNoCache(): String { + return name.replace(AppPattern.fileNameRegex, "").let { + it.substring(0, min(9, it.length)) + MD5Utils.md5Encode16(bookUrl) + } +} + +fun Book.getBookSource(): BookSource? { + return appDb.bookSourceDao.getBookSource(origin) +} + +fun Book.isLocalModified(): Boolean { + return isLocal && LocalBook.getLastModified(this).getOrDefault(0L) > latestChapterTime +} + +fun Book.releaseHtmlData() { + infoHtml = null + tocHtml = null +} + +fun Book.isSameNameAuthor(other: Any?): Boolean { + if (other is BaseBook) { + return name == other.name && author == other.author + } + return false +} + +fun Book.getExportFileName(suffix: String): String { + val jsStr = AppConfig.bookExportFileName + if (jsStr.isNullOrBlank()) { + return "$name 作者:${getRealAuthor()}.$suffix" + } + val bindings = buildScriptBindings { bindings -> + bindings["epubIndex"] = ""// 兼容老版本,修复可能存在的错误 + bindings["name"] = name + bindings["author"] = getRealAuthor() + } + return kotlin.runCatching { + RhinoScriptEngine.eval(jsStr, bindings).toString() + "." + suffix + }.onFailure { + AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it) + }.getOrDefault("$name 作者:${getRealAuthor()}.$suffix") +} + +/** + * 获取分割文件后的文件名 + */ +fun Book.getExportFileName( + suffix: String, + epubIndex: Int, + jsStr: String? = AppConfig.episodeExportFileName +): String { + // 默认规则 + val default = "$name 作者:${getRealAuthor()} [${epubIndex}].$suffix" + if (jsStr.isNullOrBlank()) { + return default + } + val bindings = buildScriptBindings { bindings -> + bindings["name"] = name + bindings["author"] = getRealAuthor() + bindings["epubIndex"] = epubIndex + } + return kotlin.runCatching { + RhinoScriptEngine.eval(jsStr, bindings).toString() + "." + suffix + }.onFailure { + AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it) + }.getOrDefault(default).normalizeFileName() +} + +// 根据当前日期计算章节总数 +fun Book.simulatedTotalChapterNum(): Int { + return if (readSimulating()) { + val currentDate = LocalDate.now() + val daysPassed = between(config.startDate, currentDate).days + 1 + // 计算当前应该解锁到哪一章 + val chaptersToUnlock = + max(0, (config.startChapter ?: 0) + (daysPassed * config.dailyChapters)) + min(totalChapterNum, chaptersToUnlock) + } else { + totalChapterNum + } +} + +fun Book.readSimulating(): Boolean { + return config.readSimulating +} + +fun tryParesExportFileName(jsStr: String): Boolean { + val bindings = buildScriptBindings { bindings -> + bindings["name"] = "name" + bindings["author"] = "author" + bindings["epubIndex"] = "epubIndex" + } + return runCatching { + RhinoScriptEngine.eval(jsStr, bindings) + true + }.getOrDefault(false) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/book/BookHelp.kt b/app/src/main/java/io/legado/app/help/book/BookHelp.kt new file mode 100755 index 000000000..ea4a5b035 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/book/BookHelp.kt @@ -0,0 +1,611 @@ +package io.legado.app.help.book + +import android.graphics.BitmapFactory +import android.os.ParcelFileDescriptor +import androidx.documentfile.provider.DocumentFile +import com.script.rhino.runScriptWithContext +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.help.config.AppConfig +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.ImageUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.StringUtils +import io.legado.app.utils.SvgUtils +import io.legado.app.utils.UrlUtil +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.exists +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getFile +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.onEachParallel +import io.legado.app.utils.postEvent +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withContext +import org.apache.commons.text.similarity.JaccardSimilarity +import splitties.init.appCtx +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap +import java.util.regex.Pattern +import java.util.zip.ZipFile +import kotlin.coroutines.coroutineContext +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +@Suppress("unused", "ConstPropertyName") +object BookHelp { + private val downloadDir: File = appCtx.externalFiles + private const val cacheFolderName = "book_cache" + private const val cacheImageFolderName = "images" + private const val cacheEpubFolderName = "epub" + private val downloadImages = ConcurrentHashMap() + + val cachePath = FileUtils.getPath(downloadDir, cacheFolderName) + + fun clearCache() { + FileUtils.delete( + FileUtils.getPath(downloadDir, cacheFolderName) + ) + } + + fun clearCache(book: Book) { + val filePath = FileUtils.getPath(downloadDir, cacheFolderName, book.getFolderName()) + FileUtils.delete(filePath) + } + + fun updateCacheFolder(oldBook: Book, newBook: Book) { + val oldFolderName = oldBook.getFolderNameNoCache() + val newFolderName = newBook.getFolderNameNoCache() + if (oldFolderName == newFolderName) return + val oldFolderPath = FileUtils.getPath( + downloadDir, + cacheFolderName, + oldFolderName + ) + val newFolderPath = FileUtils.getPath( + downloadDir, + cacheFolderName, + newFolderName + ) + FileUtils.move(oldFolderPath, newFolderPath) + } + + /** + * 清除已删除书的缓存 解压缓存 + */ + suspend fun clearInvalidCache() { + withContext(IO) { + val bookFolderNames = hashSetOf() + val originNames = hashSetOf() + appDb.bookDao.all.forEach { + clearComicCache(it) + bookFolderNames.add(it.getFolderName()) + if (it.isEpub) originNames.add(it.originName) + } + downloadDir.getFile(cacheFolderName) + .listFiles()?.forEach { bookFile -> + if (!bookFolderNames.contains(bookFile.name)) { + FileUtils.delete(bookFile.absolutePath) + } + } + downloadDir.getFile(cacheEpubFolderName) + .listFiles()?.forEach { epubFile -> + if (!originNames.contains(epubFile.name)) { + FileUtils.delete(epubFile.absolutePath) + } + } + FileUtils.delete(ArchiveUtils.TEMP_PATH) + val filesDir = appCtx.filesDir + FileUtils.delete("$filesDir/shareBookSource.json") + FileUtils.delete("$filesDir/shareRssSource.json") + FileUtils.delete("$filesDir/books.json") + } + } + + //清除已经看过的漫画数据 + private fun clearComicCache(book: Book) { + //只处理漫画 + //为0的时候,不清除已缓存数据 + if (!book.isImage || AppConfig.imageRetainNum == 0) { + return + } + //向前保留设定数量,向后保留预下载数量 + val startIndex = book.durChapterIndex - AppConfig.imageRetainNum + val endIndex = book.durChapterIndex + AppConfig.preDownloadNum + val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl, startIndex, endIndex) + val imgNames = hashSetOf() + //获取需要保留章节的图片信息 + chapterList.forEach { + val content = getContent(book, it) + if (content != null) { + val matcher = AppPattern.imgPattern.matcher(content) + while (matcher.find()) { + val src = matcher.group(1) ?: continue + val mSrc = NetworkUtils.getAbsoluteURL(it.url, src) + imgNames.add("${MD5Utils.md5Encode16(mSrc)}.${getImageSuffix(mSrc)}") + } + } + } + downloadDir.getFile( + cacheFolderName, + book.getFolderName(), + cacheImageFolderName + ).listFiles()?.forEach { imgFile -> + if (!imgNames.contains(imgFile.name)) { + imgFile.delete() + } + } + } + + suspend fun saveContent( + bookSource: BookSource, + book: Book, + bookChapter: BookChapter, + content: String + ) { + try { + saveText(book, bookChapter, content) + //saveImages(bookSource, book, bookChapter, content) + postEvent(EventBus.SAVE_CONTENT, Pair(book, bookChapter)) + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("保存正文失败 ${book.name} ${bookChapter.title}", e) + } + } + + fun saveText( + book: Book, + bookChapter: BookChapter, + content: String + ) { + if (content.isEmpty()) return + //保存文本 + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName(), + ).writeText(content) + if (book.isOnLineTxt && AppConfig.tocCountWords) { + val wordCount = StringUtils.wordCountFormat(content.length) + bookChapter.wordCount = wordCount + appDb.bookChapterDao.upWordCount(bookChapter.bookUrl, bookChapter.url, wordCount) + } + } + + fun flowImages(bookChapter: BookChapter, content: String): Flow { + return flow { + val matcher = AppPattern.imgPattern.matcher(content) + while (matcher.find()) { + val src = matcher.group(1) ?: continue + val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src) + emit(mSrc) + } + } + } + + suspend fun saveImages( + bookSource: BookSource, + book: Book, + bookChapter: BookChapter, + content: String, + concurrency: Int = AppConfig.threadCount + ) = coroutineScope { + flowImages(bookChapter, content).onEachParallel(concurrency) { mSrc -> + saveImage(bookSource, book, mSrc, bookChapter) + }.collect() + } + + suspend fun saveImage( + bookSource: BookSource?, + book: Book, + src: String, + chapter: BookChapter? = null + ) { + if (isImageExist(book, src)) { + return + } + val mutex = synchronized(this) { + downloadImages.getOrPut(src) { Mutex() } + } + mutex.lock() + try { + if (isImageExist(book, src)) { + return + } + val analyzeUrl = AnalyzeUrl( + src, source = bookSource, coroutineContext = coroutineContext + ) + val bytes = analyzeUrl.getByteArrayAwait() + //某些图片被加密,需要进一步解密 + runScriptWithContext { + ImageUtils.decode( + src, bytes, isCover = false, bookSource, book + ) + }?.let { + if (!checkImage(it)) { + // 如果部分图片失效,每次进入正文都会花很长时间再次获取图片数据 + // 所以无论如何都要将数据写入到文件里 + // throw NoStackTraceException("数据异常") + AppLog.put("${book.name} ${chapter?.title} 图片 $src 下载错误 数据异常") + } + writeImage(book, src, it) + } + } catch (e: Exception) { + coroutineContext.ensureActive() + val msg = "${book.name} ${chapter?.title} 图片 $src 下载失败\n${e.localizedMessage}" + AppLog.put(msg, e) + } finally { + downloadImages.remove(src) + mutex.unlock() + } + } + + fun getImage(book: Book, src: String): File { + return downloadDir.getFile( + cacheFolderName, + book.getFolderName(), + cacheImageFolderName, + "${MD5Utils.md5Encode16(src)}.${getImageSuffix(src)}" + ) + } + + @Synchronized + fun writeImage(book: Book, src: String, bytes: ByteArray) { + getImage(book, src).createFileIfNotExist().writeBytes(bytes) + } + + @Synchronized + fun isImageExist(book: Book, src: String): Boolean { + return getImage(book, src).exists() + } + + fun getImageSuffix(src: String): String { + return UrlUtil.getSuffix(src, "jpg") + } + + @Throws(IOException::class, FileNotFoundException::class) + fun getEpubFile(book: Book): ZipFile { + val uri = book.getLocalUri() + if (uri.isContentScheme()) { + FileUtils.createFolderIfNotExist(downloadDir, cacheEpubFolderName) + val path = FileUtils.getPath(downloadDir, cacheEpubFolderName, book.originName) + val file = File(path) + val doc = DocumentFile.fromSingleUri(appCtx, uri) + ?: throw IOException("文件不存在") + if (!file.exists() || doc.lastModified() > book.latestChapterTime) { + LocalBook.getBookInputStream(book).use { inputStream -> + FileOutputStream(file).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } + return ZipFile(file) + } + return ZipFile(uri.path) + } + + /** + * 获取本地书籍文件的ParcelFileDescriptor + * + * @param book + * @return + */ + @Throws(IOException::class, FileNotFoundException::class) + fun getBookPFD(book: Book): ParcelFileDescriptor? { + val uri = book.getLocalUri() + return if (uri.isContentScheme()) { + appCtx.contentResolver.openFileDescriptor(uri, "r") + } else { + ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY) + } + } + + fun getChapterFiles(book: Book): HashSet { + val fileNames = hashSetOf() + if (book.isLocalTxt) { + return fileNames + } + FileUtils.createFolderIfNotExist( + downloadDir, + subDirs = arrayOf(cacheFolderName, book.getFolderName()) + ).list()?.let { + fileNames.addAll(it) + } + return fileNames + } + + /** + * 检测该章节是否下载 + */ + fun hasContent(book: Book, bookChapter: BookChapter): Boolean { + return if (book.isLocalTxt || + (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) + ) { + true + } else { + downloadDir.exists( + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName() + ) + } + } + + /** + * 检测图片是否下载 + */ + fun hasImageContent(book: Book, bookChapter: BookChapter): Boolean { + if (!hasContent(book, bookChapter)) { + return false + } + var ret = true + val op = BitmapFactory.Options() + op.inJustDecodeBounds = true + getContent(book, bookChapter)?.let { + val matcher = AppPattern.imgPattern.matcher(it) + while (matcher.find()) { + val src = matcher.group(1)!! + val image = getImage(book, src) + if (!image.exists()) { + ret = false + continue + } + BitmapFactory.decodeFile(image.absolutePath, op) + if (op.outWidth < 1 && op.outHeight < 1) { + if (SvgUtils.getSize(image.absolutePath) != null) { + continue + } + ret = false + image.delete() + } + } + } + return ret + } + + private fun checkImage(bytes: ByteArray): Boolean { + val op = BitmapFactory.Options() + op.inJustDecodeBounds = true + BitmapFactory.decodeByteArray(bytes, 0, bytes.size, op) + if (op.outWidth < 1 && op.outHeight < 1) { + return SvgUtils.getSize(ByteArrayInputStream(bytes)) != null + } + return true + } + + /** + * 读取章节内容 + */ + fun getContent(book: Book, bookChapter: BookChapter): String? { + val file = downloadDir.getFile( + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName() + ) + if (file.exists()) { + val string = file.readText() + if (string.isEmpty()) { + return null + } + return string + } + if (book.isLocal) { + val string = LocalBook.getContent(book, bookChapter) + if (string != null && book.isEpub) { + saveText(book, bookChapter, string) + } + return string + } + return null + } + + /** + * 删除章节内容 + */ + fun delContent(book: Book, bookChapter: BookChapter) { + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName() + ).delete() + } + + /** + * 设置是否禁用正文的去除重复标题,针对单个章节 + */ + fun setRemoveSameTitle(book: Book, bookChapter: BookChapter, removeSameTitle: Boolean) { + val fileName = bookChapter.getFileName("nr") + val contentProcessor = ContentProcessor.get(book) + if (removeSameTitle) { + val path = FileUtils.getPath( + downloadDir, + cacheFolderName, + book.getFolderName(), + fileName + ) + contentProcessor.removeSameTitleCache.remove(fileName) + File(path).delete() + } else { + FileUtils.createFileIfNotExist( + downloadDir, + cacheFolderName, + book.getFolderName(), + fileName + ) + contentProcessor.removeSameTitleCache.add(fileName) + } + } + + /** + * 获取是否去除重复标题 + */ + fun removeSameTitle(book: Book, bookChapter: BookChapter): Boolean { + val path = FileUtils.getPath( + downloadDir, + cacheFolderName, + book.getFolderName(), + bookChapter.getFileName("nr") + ) + return !File(path).exists() + } + + /** + * 格式化书名 + */ + fun formatBookName(name: String): String { + return name + .replace(AppPattern.nameRegex, "") + .trim { it <= ' ' } + } + + /** + * 格式化作者 + */ + fun formatBookAuthor(author: String): String { + return author + .replace(AppPattern.authorRegex, "") + .trim { it <= ' ' } + } + + private val jaccardSimilarity by lazy { + JaccardSimilarity() + } + + /** + * 根据目录名获取当前章节 + */ + fun getDurChapter( + oldDurChapterIndex: Int, + oldDurChapterName: String?, + newChapterList: List, + oldChapterListSize: Int = 0 + ): Int { + if (oldDurChapterIndex <= 0) return 0 + if (newChapterList.isEmpty()) return oldDurChapterIndex + val oldChapterNum = getChapterNum(oldDurChapterName) + val oldName = getPureChapterName(oldDurChapterName) + val newChapterSize = newChapterList.size + val durIndex = + if (oldChapterListSize == 0) oldDurChapterIndex + else oldDurChapterIndex * oldChapterListSize / newChapterSize + val min = max(0, min(oldDurChapterIndex, durIndex) - 10) + val max = min(newChapterSize - 1, max(oldDurChapterIndex, durIndex) + 10) + var nameSim = 0.0 + var newIndex = 0 + var newNum = 0 + if (oldName.isNotEmpty()) { + for (i in min..max) { + val newName = getPureChapterName(newChapterList[i].title) + val temp = jaccardSimilarity.apply(oldName, newName) + if (temp > nameSim) { + nameSim = temp + newIndex = i + } + } + } + if (nameSim < 0.96 && oldChapterNum > 0) { + for (i in min..max) { + val temp = getChapterNum(newChapterList[i].title) + if (temp == oldChapterNum) { + newNum = temp + newIndex = i + break + } else if (abs(temp - oldChapterNum) < abs(newNum - oldChapterNum)) { + newNum = temp + newIndex = i + } + } + } + return if (nameSim > 0.96 || abs(newNum - oldChapterNum) < 1) { + newIndex + } else { + min(max(0, newChapterList.size - 1), oldDurChapterIndex) + } + } + + fun getDurChapter( + oldBook: Book, + newChapterList: List + ): Int { + return oldBook.run { + getDurChapter(durChapterIndex, durChapterTitle, newChapterList, totalChapterNum) + } + } + + private val chapterNamePattern1 by lazy { + Pattern.compile( + ".*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话]" + ) + } + + @Suppress("RegExpSimplifiable") + private val chapterNamePattern2 by lazy { + Pattern.compile( + "^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、]|\\.[^\\d])" + ) + } + + private val regexA by lazy { + return@lazy "\\s".toRegex() + } + + private fun getChapterNum(chapterName: String?): Int { + chapterName ?: return -1 + val chapterName1 = StringUtils.fullToHalf(chapterName).replace(regexA, "") + return StringUtils.stringToInt( + ( + chapterNamePattern1.matcher(chapterName1).takeIf { it.find() } + ?: chapterNamePattern2.matcher(chapterName1).takeIf { it.find() } + )?.group(1) + ?: "-1" + ) + } + + private val regexOther by lazy { + // 所有非字母数字中日韩文字 CJK区+扩展A-F区 + @Suppress("RegExpDuplicateCharacterInClass") + return@lazy "[^\\w\\u4E00-\\u9FEF〇\\u3400-\\u4DBF\\u20000-\\u2A6DF\\u2A700-\\u2EBEF]".toRegex() + } + + @Suppress("RegExpUnnecessaryNonCapturingGroup", "RegExpSimplifiable") + private val regexB by lazy { + //章节序号,排除处于结尾的状况,避免将章节名替换为空字串 + return@lazy "^.*?第(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话](?!$)|^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、](?!$)|\\.(?=[^\\d]))".toRegex() + } + + private val regexC by lazy { + //前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串 + return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex() + } + + private fun getPureChapterName(chapterName: String?): String { + return if (chapterName == null) "" else StringUtils.fullToHalf(chapterName) + .replace(regexA, "") + .replace(regexB, "") + .replace(regexC, "") + .replace(regexOther, "") + } + +} diff --git a/app/src/main/java/io/legado/app/help/book/ContentHelp.kt b/app/src/main/java/io/legado/app/help/book/ContentHelp.kt new file mode 100755 index 000000000..1f2cd8fd4 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/book/ContentHelp.kt @@ -0,0 +1,630 @@ +package io.legado.app.help.book + +import java.util.regex.Pattern +import kotlin.math.max +import kotlin.math.min + +@Suppress("SameParameterValue", "RegExpRedundantEscape") +object ContentHelp { + + /** + * 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分 + * + * @param content 正文 + * @param chapterName 标题 + * @return + */ + fun reSegment(content: String, chapterName: String): String { + var content1 = content + val dict = makeDict(content1) + var p = content1 + .replace(""".toRegex(), "“") + .replace("[::]['\"‘”“]+".toRegex(), ":“") + .replace("[\"”“]+\\s*[\"”“][\\s\"”“]*".toRegex(), "”\n“") + .split("\n(\\s*)".toRegex()).toTypedArray() + + //初始化StringBuilder的长度,在原content的长度基础上做冗余 + var buffer = StringBuilder((content1.length * 1.15).toInt()) + // 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。 + buffer.append(" ") + if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) { + // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 + buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), "")) + } + + //如果原文存在分段错误,需要把段落重新黏合 + for (i in 1 until p.size) { + if (match(MARK_SENTENCES_END, buffer.last()) + || (match(MARK_QUOTATION_RIGHT, buffer.last()) + && match(MARK_SENTENCES_END, buffer[buffer.lastIndex - 1])) + ) { + buffer.append("\n") + } + // 段落开头以外的地方不应该有空格 + // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 + buffer.append(p[i].replace("[\u3000\\s]".toRegex(), "")) + } + // 预分段预处理 + // ”“处理为”\n“。 + // ”。“处理为”。\n“。不考虑“?” “!”的情况。 + // ”。xxx处理为 ”。\n xxx + p = buffer.toString() + .replace("[\"”“]+\\s*[\"”“]+".toRegex(), "”\n“") + .replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "”$1\n“") + .replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "”$1\n$2") + .replace( + "([问说喊唱叫骂道着答])[\\.。]".toRegex(), + "$1。\n" + ) + .split("\n".toRegex()).toTypedArray() + buffer = StringBuilder((content1.length * 1.15).toInt()) + for (s in p) { + buffer.append("\n") + buffer.append(findNewLines(s, dict)) + } + buffer = reduceLength(buffer) + content1 = (buffer.toString() // 处理章节头部空格和换行 + .replaceFirst("^\\s+".toRegex(), "") + .replace("\\s*[\"”“]+\\s*[\"”“][\\s\"”“]*".toRegex(), "”\n“") + .replace("[::][”“\"\\s]+".toRegex(), ":“") + .replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3") + .replace("\n(\\s*)".toRegex(), "\n")) + return content1 + } + + /** + * 强制切分,减少段落内的句子 + * 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落 + * 如果引号内的内容长于5句,可能引号状态有误,随机分段 + * 如果引号外的内容长于3句,随机分段 + * + * @param str + * @return + */ + private fun reduceLength(str: StringBuilder): StringBuilder { + val p = str.toString().split("\n".toRegex()).toTypedArray() + val l = p.size + val b = BooleanArray(l) + for (i in 0 until l) { + b[i] = p[i].matches(PARAGRAPH_DIAGLOG) + } + var dialogue = 0 + for (i in 0 until l) { + if (b[i]) { + if (dialogue < 0) dialogue = 1 else if (dialogue < 2) dialogue++ + } else { + if (dialogue > 1) { + p[i] = splitQuote(p[i]) + dialogue-- + } else if (dialogue > 0 && i < l - 2) { + if (b[i + 1]) p[i] = splitQuote(p[i]) + } + } + } + val string = StringBuilder() + for (i in 0 until l) { + string.append('\n') + string.append(p[i]) + //System.out.print(" "+b[i]); + } + //System.out.println(" " + str); + return string + } + + // 强制切分进入对话模式后,未构成 “xxx” 形式的段落 + private fun splitQuote(str: String): String { + val length = str.length + if (length < 3) return str + if (match(MARK_QUOTATION, str[0])) { + val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1 + if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) { + return "${str.take(i)}\n${str.substring(i)}" + } + } else if (match(MARK_QUOTATION, str[length - 1])) { + val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false) + if (i > 1) { + if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) { + return "${str.take(i)}\n${str.substring(i)}" + } + } + } + return str + } + + /** + * 计算随机插入换行符的位置。 + * @param str 字符串 + * @param offset 传回的结果需要叠加的偏移量 + * @param min 最低几个句子,随机插入换行 + * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行 + * @return + */ + private fun forceSplit( + str: String, + offset: Int, + min: Int, + gain: Int, + tigger: Int + ): ArrayList { + val result = ArrayList() + val arrayEnd = seekIndexes(str, MARK_SENTENCES_END_P, 0, str.length - 2, true) + val arrayMid = seekIndexes(str, MARK_SENTENCES_MID, 0, str.length - 2, true) + if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result + var j = 0 + var i = min + while (i < arrayEnd.size) { + var k = 0 + while (j < arrayMid.size) { + if (arrayMid[j] < arrayEnd[i]) k++ + j++ + } + if (Math.random() * gain < 0.8 + k / 2.5) { + result.add(arrayEnd[i] + offset) + i = max(i + min, i) + } + i++ + } + return result + } + + // 对内容重新划分段落.输入参数str已经使用换行符预分割 + private fun findNewLines(str: String, dict: List): String { + val string = StringBuilder(str) + // 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。 + val arrayQuote: MutableList = ArrayList() + // 标记插入换行符的位置,int为插入位置(str的char下标) + var insN = ArrayList() + + //mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。 + //长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间 + //0未知,正数引号内,负数引号外。 + //如果相邻的两个标记都为+1,那么需要增加1个引号。 + //引号内不进行断句 + val mod = IntArray(str.length) + var waitClose = false + for (i in str.indices) { + val c = str[i] + if (match(MARK_QUOTATION, c)) { + val size = arrayQuote.size + + // 把“xxx”、“yy”合并为“xxx_yy”进行处理 + if (size > 0) { + val quotePre = arrayQuote[size - 1] + if (i - quotePre == 2) { + var remove = false + if (waitClose) { + if (match(",,、/", str[i - 1])) { + // 考虑出现“和”这种特殊情况 + remove = true + } + } else if (match(",,、/和与或", str[i - 1])) { + remove = true + } + if (remove) { + string.setCharAt(i, '“') + string.setCharAt(i - 2, '”') + arrayQuote.removeAt(size - 1) + mod[size - 1] = 1 + mod[size] = -1 + continue + } + } + } + arrayQuote.add(i) + + // 为xxx:“xxx”做标记 + if (i > 1) { + // 当前发言的正引号的前一个字符 + val charB1 = str[i - 1] + // 上次发言的正引号的前一个字符 + var charB2 = 0.toChar() + if (match(MARK_QUOTATION_BEFORE, charB1)) { + // 如果不是第一处引号,寻找上一处断句,进行分段 + if (arrayQuote.size > 1) { + val lastQuote = arrayQuote[arrayQuote.size - 2] + var p = 0 + if (charB1 == ',' || charB1 == ',') { + if (arrayQuote.size > 2) { + p = arrayQuote[arrayQuote.size - 3] + if (p > 0) { + charB2 = str[p - 1] + } + } + } + //if(char_b2=='.' || char_b2=='。') + if (match(MARK_SENTENCES_END_P, charB2)) { + insN.add(p - 1) + } else if (!match("的", charB2)) { + val lastEnd = seekLast(str, MARK_SENTENCES_END, i, lastQuote) + if (lastEnd > 0) insN.add(lastEnd) else insN.add(lastQuote) + } + } + waitClose = true + mod[size] = 1 + if (size > 0) { + mod[size - 1] = -1 + if (size > 1) { + mod[size - 2] = 1 + } + } + } else if (waitClose) { + run { + waitClose = false + insN.add(i) + } + } + } + } + } + val size = arrayQuote.size + + + //标记循环状态,此位置前的引号是否已经配对 + var opend = false + if (size > 0) { + //第1次遍历array_quote,令其元素的值不为0 + for (i in 0 until size) { + if (mod[i] > 0) { + opend = true + } else if (mod[i] < 0) { + //连续2个反引号表明存在冲突,强制把前一个设为正引号 + if (!opend) { + if (i > 0) mod[i] = 3 + } + opend = false + } else { + opend = !opend + if (opend) mod[i] = 2 else mod[i] = -2 + } + } + // 修正,断尾必须封闭引号 + if (opend) { + if (arrayQuote[size - 1] - string.length > -3) { + //if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){ + if (size > 1) mod[size - 2] = 4 + // 0<=i=1 + mod[size - 1] = -4 + } else if (!match(MARK_SENTENCES_SAY, string[string.length - 2])) string.append( + "”" + ) + } + + + //第2次循环,mod[i]由负变正时,前1字符如果是句末,需要插入换行 + var loop2Mod1 = -1 //上一个引号跟随内容的状态 + var loop2Mod2: Int //当前引号跟随内容的状态 + var i = 0 + var j = arrayQuote[0] - 1 //当前引号前一字符的序号 + if (j < 0) { + i = 1 + loop2Mod1 = 0 + } + while (i < size) { + j = arrayQuote[i] - 1 + loop2Mod2 = mod[i] + if (loop2Mod1 < 0 && loop2Mod2 > 0) { + if (match(MARK_SENTENCES_END, string[j])) insN.add(j) + } + loop2Mod1 = loop2Mod2 + i++ + } + } + + //第3次循环,匹配并插入换行。 + //"xxxx" xxxx。\n xxx“xxxx” + //未实现 + + // 使用字典验证ins_n , 避免插入不必要的换行。 + // 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题 + val insN1 = ArrayList() + for (i in insN) { + if (match("\"'”“", string[i])) { + val start: Int = seekLast( + str, + "\"'”“", + i - 1, + i - WORD_MAX_LENGTH + ) + if (start > 0) { + val word = str.substring(start + 1, i) + if (dict.contains(word)) { + //System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word); + //引号内如果是字典词条,后方不插入换行符(前方不需要优化) + continue + } else { + //System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word); + if (match("的地得", str[start])) { + //xx的“xx”,后方不插入换行符(前方不需要优化) + continue + } + } + } + } + insN1.add(i) + } + insN = insN1 + +// 随机在句末插入换行符 + insN = ArrayList(HashSet(insN)) + insN.sort() + run { + var subs: String + var j = 0 + var progress = 0 + var nextLine = -1 + if (insN.isNotEmpty()) nextLine = insN[j] + var gain = 3 + var min = 0 + var trigger = 2 + for (i in arrayQuote.indices) { + val qutoe = arrayQuote[i] + if (qutoe > 0) { + gain = 4 + min = 2 + trigger = 4 + } else { + gain = 3 + min = 0 + trigger = 2 + } + +// 把引号前的换行符与内容相间插入 + while (j < insN.size) { + +// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 + if (nextLine >= qutoe) break + nextLine = insN[j] + if (progress < nextLine) { + subs = string.substring(progress, nextLine) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = nextLine + 1 + } + j++ + } + if (progress < qutoe) { + subs = string.substring(progress, qutoe + 1) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = qutoe + 1 + } + } + while (j < insN.size) { + nextLine = insN[j] + if (progress < nextLine) { + subs = string.substring(progress, nextLine) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = nextLine + 1 + } + j++ + } + if (progress < string.length) { + subs = string.substring(progress, string.length) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + } + } + +// 根据段落状态修正引号方向、计算需要插入引号的位置 +// ins_quote跟随array_quote ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' + val insQuote = BooleanArray(size) + opend = false + for (i in 0 until size) { + val p = arrayQuote[i] + if (mod[i] > 0) { + string.setCharAt(p, '“') + if (opend) insQuote[i] = true + opend = true + } else if (mod[i] < 0) { + string.setCharAt(p, '”') + opend = false + } else { + opend = !opend + if (opend) string.setCharAt(p, '“') else string.setCharAt(p, '”') + } + } + insN = ArrayList(HashSet(insN)) + insN.sort() + +// 完成字符串拼接(从string复制、插入引号和换行 +// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' +// ins_n 插入换行。数组的值表示插入换行符的位置 + val buffer = StringBuilder((str.length * 1.15).toInt()) + var j = 0 + var progress = 0 + var nextLine = -1 + if (insN.isNotEmpty()) nextLine = insN[j] + for (i in arrayQuote.indices) { + val quote = arrayQuote[i] + +// 把引号前的换行符与内容相间插入 + while (j < insN.size) { + +// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 + if (nextLine >= quote) break + nextLine = insN[j] + buffer.append(string, progress, nextLine + 1) + buffer.append('\n') + progress = nextLine + 1 + j++ + } + if (progress < quote) { + buffer.append(string, progress, quote + 1) + progress = quote + 1 + } + if (insQuote[i] && buffer.length > 2) { + if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert( + buffer.length - 1, + "”\n" + ) + } + } + while (j < insN.size) { + nextLine = insN[j] + if (progress <= nextLine) { + buffer.append(string, progress, nextLine + 1) + buffer.append('\n') + progress = nextLine + 1 + } + j++ + } + if (progress < string.length) { + buffer.append(string, progress, string.length) + } + return buffer.toString() + } + + /** + * 从字符串提取引号包围,且不止出现一次的内容为字典 + * + * @param str + * @return 词条列表 + */ + private fun makeDict(str: String): List { + + // 引号中间不包含任何标点 + val patten = Pattern.compile( + """ + (?<=["'”“])([^ + \p{P}]{1,$WORD_MAX_LENGTH})(?=["'”“]) + """.trimIndent() + ) + //Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\"'”“]{1,16})(?=[\"'”“])"); + val matcher = patten.matcher(str) + val cache: MutableList = ArrayList() + val dict: MutableList = ArrayList() + while (matcher.find()) { + val word = matcher.group() + if (cache.contains(word)) { + if (!dict.contains(word)) dict.add(word) + } else cache.add(word) + } + return dict + } + + /** + * 计算匹配到字典的每个字符的位置 + * + * @param str 待匹配的字符串 + * @param key 字典 + * @param from 从字符串的第几个字符开始匹配 + * @param to 匹配到第几个字符结束 + * @param inOrder 是否按照从前向后的顺序匹配 + * @return 返回距离构成的ArrayList + */ + private fun seekIndexes( + str: String, + key: String, + from: Int, + to: Int, + inOrder: Boolean + ): ArrayList { + val list = ArrayList() + if (str.length - from < 1) return list + var i = 0 + if (from > 0) i = from + var t = str.length + if (to > 0) t = min(t, to) + var c: Char + while (i < t) { + c = if (inOrder) str[i] else str[str.length - i - 1] + if (key.indexOf(c) != -1) { + if (list.isNotEmpty() && i - list.last() == 1) { + list[list.lastIndex] = i + } else { + list.add(i) + } + } + i++ + } + return list + } + + /** + * 计算字符串最后出现与字典中字符匹配的位置 + * + * @param str 数据字符串 + * @param key 字典字符串 + * @param from 从哪个字符开始匹配,默认最末位 + * @param to 匹配到哪个字符(不包含此字符)默认0 + * @return 位置(正向计算) + */ + private fun seekLast(str: String, key: String, from: Int, to: Int): Int { + if (str.length - from < 1) return -1 + var i = str.lastIndex + if (from < i && i > 0) i = from + var t = 0 + if (to > 0) t = to + var c: Char + while (i > t) { + c = str[i] + if (key.indexOf(c) != -1) { + return i + } + i-- + } + return -1 + } + + /** + * 计算字符串与字典中字符的最短距离 + * + * @param str 数据字符串 + * @param key 字典字符串 + * @param from 从哪个字符开始匹配,默认0 + * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位 + * @param inOrder 是否从正向开始匹配 + * @return 返回最短距离, 注意不是str的char的下标 + */ + private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int { + if (str.length - from < 1) return -1 + var i = 0 + if (from > 0) i = from + var t = str.length + if (to > 0) t = min(t, to) + var c: Char + while (i < t) { + c = if (inOrder) str[i] else str[str.length - i - 1] + if (key.indexOf(c) != -1) { + return i + } + i++ + } + return -1 + } + + /* 搜寻引号并进行分段。处理了一、二、五三类常见情况 + 参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。 + 一、完整引用说话内容,在反引号内侧有断句标点。例如: + 1) 丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!” + 2)“哎呀,真是美极了!”皇帝说,“我十分满意!” + 3)“怕什么!海的美就在这里!”我说道。 + 二、部分引用,在反引号外侧有断句标点: + 4)适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。 + 5)现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。 + 6)唐朝的张嘉贞说它“制造奇特,人不知其所为”。 + 三、一段接着一段地直接引用时,中间段落只在段首用起引号,该段段尾却不用引回号。但是正统文学不在考虑范围内。 + 四、引号里面又要用引号时,外面一层用双引号,里面一层用单引号。暂时不需要考虑 + 五、反语和强调,周围没有断句符号。 + */ + + // 句子结尾的标点。因为引号可能存在误判,不包含引号。 + private const val MARK_SENTENCES_END = "?。!?!~" + private const val MARK_SENTENCES_END_P = ".?。!?!~" + + // 句中标点,由于某些网站常把“,”写为".",故英文句点按照句中标点判断 + private const val MARK_SENTENCES_MID = ".,、,—…" + private const val MARK_SENTENCES_SAY = "问说喊唱叫骂道着答" + + // XXX说:“”的冒号 + private const val MARK_QUOTATION_BEFORE = ",:,:" + + // 引号 + private const val MARK_QUOTATION = "\"“”" + private const val MARK_QUOTATION_RIGHT = "\"”" + private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex() + + // 限制字典的长度 + private const val WORD_MAX_LENGTH = 16 + + private fun match(rule: String, chr: Char): Boolean { + return rule.indexOf(chr) != -1 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt b/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt new file mode 100755 index 000000000..978f16423 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt @@ -0,0 +1,206 @@ +package io.legado.app.help.book + +import android.os.Build +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern.spaceRegex +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.exception.RegexTimeoutException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.utils.ChineseUtils +import io.legado.app.utils.escapeRegex +import io.legado.app.utils.replace +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CancellationException +import splitties.init.appCtx +import java.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList +import java.util.regex.Pattern + +class ContentProcessor private constructor( + private val bookName: String, + private val bookOrigin: String +) { + + companion object { + private val processors = hashMapOf>() + private val isAndroid8 = Build.VERSION.SDK_INT in 26..27 + + fun get(book: Book) = get(book.name, book.origin) + + fun get(bookName: String, bookOrigin: String): ContentProcessor { + val processorWr = processors[bookName + bookOrigin] + var processor: ContentProcessor? = processorWr?.get() + if (processor == null) { + processor = ContentProcessor(bookName, bookOrigin) + processors[bookName + bookOrigin] = WeakReference(processor) + } + return processor + } + + fun upReplaceRules() { + processors.forEach { + it.value.get()?.upReplaceRules() + } + } + + } + + private val titleReplaceRules = CopyOnWriteArrayList() + private val contentReplaceRules = CopyOnWriteArrayList() + val removeSameTitleCache = hashSetOf() + + init { + upReplaceRules() + upRemoveSameTitle() + } + + fun upReplaceRules() { + titleReplaceRules.run { + clear() + addAll(appDb.replaceRuleDao.findEnabledByTitleScope(bookName, bookOrigin)) + } + contentReplaceRules.run { + clear() + addAll(appDb.replaceRuleDao.findEnabledByContentScope(bookName, bookOrigin)) + } + } + + private fun upRemoveSameTitle() { + val book = appDb.bookDao.getBookByOrigin(bookName, bookOrigin) ?: return + removeSameTitleCache.clear() + val files = BookHelp.getChapterFiles(book).filter { + it.endsWith("nr") + } + removeSameTitleCache.addAll(files) + } + + fun getTitleReplaceRules(): List { + return titleReplaceRules + } + + @Suppress("MemberVisibilityCanBePrivate") + fun getContentReplaceRules(): List { + return contentReplaceRules + } + + fun getContent( + book: Book, + chapter: BookChapter, + content: String, + includeTitle: Boolean = true, + useReplace: Boolean = true, + chineseConvert: Boolean = true, + reSegment: Boolean = true + ): BookContent { + var mContent = content + var sameTitleRemoved = false + var effectiveReplaceRules: ArrayList? = null + if (content != "null") { + //去除重复标题 + val fileName = chapter.getFileName("nr") + if (!removeSameTitleCache.contains(fileName)) try { + val name = Pattern.quote(book.name) + var title = chapter.title.escapeRegex().replace(spaceRegex, "\\\\s*") + var matcher = Pattern.compile("^(\\s|\\p{P}|${name})*${title}(\\s)*") + .matcher(mContent) + if (matcher.find()) { + mContent = mContent.substring(matcher.end()) + sameTitleRemoved = true + } else if (useReplace && book.getUseReplaceRule()) { + title = Pattern.quote( + chapter.getDisplayTitle( + titleReplaceRules, + chineseConvert = false + ) + ) + matcher = Pattern.compile("^(\\s|\\p{P}|${name})*${title}(\\s)*") + .matcher(mContent) + if (matcher.find()) { + mContent = mContent.substring(matcher.end()) + sameTitleRemoved = true + } + } + } catch (e: Exception) { + AppLog.put("去除重复标题出错\n${e.localizedMessage}", e) + } + if (reSegment && book.getReSegment()) { + //重新分段 + mContent = ContentHelp.reSegment(mContent, chapter.title) + } + if (chineseConvert) { + //简繁转换 + try { + when (AppConfig.chineseConverterType) { + 1 -> mContent = ChineseUtils.t2s(mContent) + 2 -> mContent = ChineseUtils.s2t(mContent) + } + } catch (_: Exception) { + appCtx.toastOnUi("简繁转换出错") + } + } + if (useReplace && book.getUseReplaceRule()) { + //替换 + effectiveReplaceRules = arrayListOf() + mContent = mContent.lines().joinToString("\n") { it.trim() } + getContentReplaceRules().forEach { item -> + if (item.pattern.isEmpty()) { + return@forEach + } + try { + val tmp = if (item.isRegex) { + mContent.replace( + item.regex, + item.replacement, + item.getValidTimeoutMillisecond() + ) + } else { + mContent.replace(item.pattern, item.replacement) + } + if (mContent != tmp) { + effectiveReplaceRules.add(item) + mContent = tmp + } + } catch (e: RegexTimeoutException) { + item.isEnabled = false + appDb.replaceRuleDao.update(item) + mContent = item.name + e.stackTraceStr + } catch (_: CancellationException) { + } catch (e: Exception) { + AppLog.put("替换净化: 规则 ${item.name}替换出错.\n${mContent}", e) + appCtx.toastOnUi("替换净化: 规则 ${item.name}替换出错") + } + } + } + } + if (includeTitle) { + //重新添加标题 + mContent = chapter.getDisplayTitle( + getTitleReplaceRules(), + useReplace = useReplace && book.getUseReplaceRule() + ) + "\n" + mContent + } + if (isAndroid8) { + mContent = mContent.replace('\u00A0', ' ') + } + val contents = arrayListOf() + mContent.split("\n").forEach { str -> + val paragraph = str.trim { + it.code <= 0x20 || it == ' ' + } + if (paragraph.isNotEmpty()) { + if (contents.isEmpty() && includeTitle) { + contents.add(paragraph) + } else { + contents.add("${ReadBookConfig.paragraphIndent}$paragraph") + } + } + } + return BookContent(sameTitleRemoved, contents, effectiveReplaceRules) + } + +} diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt new file mode 100755 index 000000000..bd7492953 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -0,0 +1,742 @@ +package io.legado.app.help.config + +import android.content.SharedPreferences +import android.os.Build +import io.legado.app.BuildConfig +import io.legado.app.constant.AppConst +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.getPrefLong +import io.legado.app.utils.getPrefString +import io.legado.app.utils.isNightMode +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.putPrefLong +import io.legado.app.utils.putPrefString +import io.legado.app.utils.removePref +import io.legado.app.utils.sysConfiguration +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +@Suppress("MemberVisibilityCanBePrivate", "ConstPropertyName") +object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { + val isCronet = appCtx.getPrefBoolean(PreferKey.cronet) + var useAntiAlias = appCtx.getPrefBoolean(PreferKey.antiAlias) + var userAgent: String = getPrefUserAgent() + var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3" + var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2) + var clickActionTC = appCtx.getPrefInt(PreferKey.clickActionTC, 2) + var clickActionTR = appCtx.getPrefInt(PreferKey.clickActionTR, 1) + var clickActionML = appCtx.getPrefInt(PreferKey.clickActionML, 2) + var clickActionMC = appCtx.getPrefInt(PreferKey.clickActionMC, 0) + var clickActionMR = appCtx.getPrefInt(PreferKey.clickActionMR, 1) + var clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2) + var clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1) + var clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1) + var themeMode = appCtx.getPrefString(PreferKey.themeMode, "0") + var useDefaultCover = appCtx.getPrefBoolean(PreferKey.useDefaultCover, false) + var optimizeRender = CanvasRecorderFactory.isSupport + && appCtx.getPrefBoolean(PreferKey.optimizeRender, false) + var recordLog = appCtx.getPrefBoolean(PreferKey.recordLog) + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.themeMode -> { + themeMode = appCtx.getPrefString(PreferKey.themeMode, "0") + isEInkMode = themeMode == "3" + } + + PreferKey.clickActionTL -> clickActionTL = + appCtx.getPrefInt(PreferKey.clickActionTL, 2) + + PreferKey.clickActionTC -> clickActionTC = + appCtx.getPrefInt(PreferKey.clickActionTC, 2) + + PreferKey.clickActionTR -> clickActionTR = + appCtx.getPrefInt(PreferKey.clickActionTR, 1) + + PreferKey.clickActionML -> clickActionML = + appCtx.getPrefInt(PreferKey.clickActionML, 2) + + PreferKey.clickActionMC -> clickActionMC = + appCtx.getPrefInt(PreferKey.clickActionMC, 0) + + PreferKey.clickActionMR -> clickActionMR = + appCtx.getPrefInt(PreferKey.clickActionMR, 1) + + PreferKey.clickActionBL -> clickActionBL = + appCtx.getPrefInt(PreferKey.clickActionBL, 2) + + PreferKey.clickActionBC -> clickActionBC = + appCtx.getPrefInt(PreferKey.clickActionBC, 1) + + PreferKey.clickActionBR -> clickActionBR = + appCtx.getPrefInt(PreferKey.clickActionBR, 1) + + PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh = + appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + + PreferKey.useZhLayout -> ReadBookConfig.useZhLayout = + appCtx.getPrefBoolean(PreferKey.useZhLayout) + + PreferKey.userAgent -> userAgent = getPrefUserAgent() + + PreferKey.antiAlias -> useAntiAlias = appCtx.getPrefBoolean(PreferKey.antiAlias) + + PreferKey.useDefaultCover -> useDefaultCover = + appCtx.getPrefBoolean(PreferKey.useDefaultCover, false) + + PreferKey.optimizeRender -> optimizeRender = CanvasRecorderFactory.isSupport + && appCtx.getPrefBoolean(PreferKey.optimizeRender, false) + + PreferKey.recordLog -> recordLog = appCtx.getPrefBoolean(PreferKey.recordLog) + + } + } + + var isNightTheme: Boolean + get() = when (themeMode) { + "1" -> false + "2" -> true + "3" -> false + else -> sysConfiguration.isNightMode + } + set(value) { + if (isNightTheme != value) { + if (value) { + appCtx.putPrefString(PreferKey.themeMode, "2") + } else { + appCtx.putPrefString(PreferKey.themeMode, "1") + } + } + } + + var showUnread: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showUnread, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.showUnread, value) + } + + var showLastUpdateTime: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showLastUpdateTime, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.showLastUpdateTime, value) + } + + var showWaitUpCount: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showWaitUpCount, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.showWaitUpCount, value) + } + + var readBrightness: Int + get() = if (isNightTheme) { + appCtx.getPrefInt(PreferKey.nightBrightness, 100) + } else { + appCtx.getPrefInt(PreferKey.brightness, 100) + } + set(value) { + if (isNightTheme) { + appCtx.putPrefInt(PreferKey.nightBrightness, value) + } else { + appCtx.putPrefInt(PreferKey.brightness, value) + } + } + + val textSelectAble: Boolean + get() = appCtx.getPrefBoolean(PreferKey.textSelectAble, true) + + val isTransparentStatusBar: Boolean + get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true) + + val immNavigationBar: Boolean + get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true) + + val screenOrientation: String? + get() = appCtx.getPrefString(PreferKey.screenOrientation) + + var bookGroupStyle: Int + get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0) + set(value) { + appCtx.putPrefInt(PreferKey.bookGroupStyle, value) + } + + var bookshelfLayout: Int + get() = appCtx.getPrefInt(PreferKey.bookshelfLayout, 0) + set(value) { + appCtx.putPrefInt(PreferKey.bookshelfLayout, value) + } + + var saveTabPosition: Int + get() = appCtx.getPrefInt(PreferKey.saveTabPosition, 0) + set(value) { + appCtx.putPrefInt(PreferKey.saveTabPosition, value) + } + + var bookExportFileName: String? + get() = appCtx.getPrefString(PreferKey.bookExportFileName) + set(value) { + appCtx.putPrefString(PreferKey.bookExportFileName, value) + } + + // 保存 自定义导出章节模式 文件名js表达式 + var episodeExportFileName: String? + get() = appCtx.getPrefString(PreferKey.episodeExportFileName, "") + set(value) { + appCtx.putPrefString(PreferKey.episodeExportFileName, value) + } + + var bookImportFileName: String? + get() = appCtx.getPrefString(PreferKey.bookImportFileName) + set(value) { + appCtx.putPrefString(PreferKey.bookImportFileName, value) + } + + var backupPath: String? + get() = appCtx.getPrefString(PreferKey.backupPath) + set(value) { + if (value.isNullOrEmpty()) { + appCtx.removePref(PreferKey.backupPath) + } else { + appCtx.putPrefString(PreferKey.backupPath, value) + } + } + + // 书籍保存位置 + var defaultBookTreeUri: String? + get() = appCtx.getPrefString(PreferKey.defaultBookTreeUri) + set(value) { + if (value.isNullOrEmpty()) { + appCtx.removePref(PreferKey.defaultBookTreeUri) + } else { + appCtx.putPrefString(PreferKey.defaultBookTreeUri, value) + } + } + + val showDiscovery: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true) + + val showRSS: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showRss, true) + + val autoRefreshBook: Boolean + get() = appCtx.getPrefBoolean(PreferKey.autoRefresh) + + var enableReview: Boolean + get() = BuildConfig.DEBUG && appCtx.getPrefBoolean(PreferKey.enableReview, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.enableReview, value) + } + + var threadCount: Int + get() = appCtx.getPrefInt(PreferKey.threadCount, 16) + set(value) { + appCtx.putPrefInt(PreferKey.threadCount, value) + } + + var remoteServerId: Long + get() = appCtx.getPrefLong(PreferKey.remoteServerId) + set(value) { + appCtx.putPrefLong(PreferKey.remoteServerId, value) + } + + // 添加本地选择的目录 + var importBookPath: String? + get() = appCtx.getPrefString("importBookPath") + set(value) { + if (value == null) { + appCtx.removePref("importBookPath") + } else { + appCtx.putPrefString("importBookPath", value) + } + } + + var ttsFlowSys: Boolean + get() = appCtx.getPrefBoolean(PreferKey.ttsFollowSys, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.ttsFollowSys, value) + } + + val noAnimScrollPage: Boolean + get() = appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false) + + const val defaultSpeechRate = 5 + + var ttsSpeechRate: Int + get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, defaultSpeechRate) + set(value) { + appCtx.putPrefInt(PreferKey.ttsSpeechRate, value) + } + + var ttsTimer: Int + get() = appCtx.getPrefInt(PreferKey.ttsTimer, 0) + set(value) { + appCtx.putPrefInt(PreferKey.ttsTimer, value) + } + + val speechRatePlay: Int get() = if (ttsFlowSys) defaultSpeechRate else ttsSpeechRate + + var chineseConverterType: Int + get() = appCtx.getPrefInt(PreferKey.chineseConverterType) + set(value) { + appCtx.putPrefInt(PreferKey.chineseConverterType, value) + } + + var systemTypefaces: Int + get() = appCtx.getPrefInt(PreferKey.systemTypefaces) + set(value) { + appCtx.putPrefInt(PreferKey.systemTypefaces, value) + } + + var elevation: Int + get() = if (isEInkMode) 0 else appCtx.getPrefInt( + PreferKey.barElevation, + AppConst.sysElevation + ) + set(value) { + appCtx.putPrefInt(PreferKey.barElevation, value) + } + + var readUrlInBrowser: Boolean + get() = appCtx.getPrefBoolean(PreferKey.readUrlOpenInBrowser) + set(value) { + appCtx.putPrefBoolean(PreferKey.readUrlOpenInBrowser, value) + } + + var exportCharset: String + get() { + val c = appCtx.getPrefString(PreferKey.exportCharset) + if (c.isNullOrBlank()) { + return "UTF-8" + } + return c + } + set(value) { + appCtx.putPrefString(PreferKey.exportCharset, value) + } + + var exportUseReplace: Boolean + get() = appCtx.getPrefBoolean(PreferKey.exportUseReplace, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.exportUseReplace, value) + } + + var exportToWebDav: Boolean + get() = appCtx.getPrefBoolean(PreferKey.exportToWebDav) + set(value) { + appCtx.putPrefBoolean(PreferKey.exportToWebDav, value) + } + var exportNoChapterName: Boolean + get() = appCtx.getPrefBoolean(PreferKey.exportNoChapterName) + set(value) { + appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value) + } + + // 是否启用自定义导出 default->false + var enableCustomExport: Boolean + get() = appCtx.getPrefBoolean(PreferKey.enableCustomExport, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.enableCustomExport, value) + } + + var exportType: Int + get() = appCtx.getPrefInt(PreferKey.exportType) + set(value) { + appCtx.putPrefInt(PreferKey.exportType, value) + } + var exportPictureFile: Boolean + get() = appCtx.getPrefBoolean(PreferKey.exportPictureFile, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.exportPictureFile, value) + } + + var parallelExportBook: Boolean + get() = appCtx.getPrefBoolean(PreferKey.parallelExportBook, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.parallelExportBook, value) + } + + var changeSourceCheckAuthor: Boolean + get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor) + set(value) { + appCtx.putPrefBoolean(PreferKey.changeSourceCheckAuthor, value) + } + + var ttsEngine: String? + get() = appCtx.getPrefString(PreferKey.ttsEngine) + set(value) { + appCtx.putPrefString(PreferKey.ttsEngine, value) + } + + var webPort: Int + get() = appCtx.getPrefInt(PreferKey.webPort, 1122) + set(value) { + appCtx.putPrefInt(PreferKey.webPort, value) + } + + var tocUiUseReplace: Boolean + get() = appCtx.getPrefBoolean(PreferKey.tocUiUseReplace) + set(value) { + appCtx.putPrefBoolean(PreferKey.tocUiUseReplace, value) + } + + var tocCountWords: Boolean + get() = appCtx.getPrefBoolean(PreferKey.tocCountWords, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.tocCountWords, value) + } + + var enableReadRecord: Boolean + get() = appCtx.getPrefBoolean(PreferKey.enableReadRecord, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.enableReadRecord, value) + } + + val autoChangeSource: Boolean + get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true) + + var changeSourceLoadInfo: Boolean + get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo) + set(value) { + appCtx.putPrefBoolean(PreferKey.changeSourceLoadInfo, value) + } + + var changeSourceLoadToc: Boolean + get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc) + set(value) { + appCtx.putPrefBoolean(PreferKey.changeSourceLoadToc, value) + } + + var changeSourceLoadWordCount: Boolean + get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadWordCount) + set(value) { + appCtx.putPrefBoolean(PreferKey.changeSourceLoadWordCount, value) + } + + var openBookInfoByClickTitle: Boolean + get() = appCtx.getPrefBoolean(PreferKey.openBookInfoByClickTitle, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.openBookInfoByClickTitle, value) + } + + var showBookshelfFastScroller: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showBookshelfFastScroller, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.showBookshelfFastScroller, value) + } + + var contentSelectSpeakMod: Int + get() = appCtx.getPrefInt(PreferKey.contentSelectSpeakMod) + set(value) { + appCtx.putPrefInt(PreferKey.contentSelectSpeakMod, value) + } + + var batchChangeSourceDelay: Int + get() = appCtx.getPrefInt(PreferKey.batchChangeSourceDelay) + set(value) { + appCtx.putPrefInt(PreferKey.batchChangeSourceDelay, value) + } + + val importKeepName get() = appCtx.getPrefBoolean(PreferKey.importKeepName) + val importKeepGroup get() = appCtx.getPrefBoolean(PreferKey.importKeepGroup) + var importKeepEnable: Boolean + get() = appCtx.getPrefBoolean(PreferKey.importKeepEnable, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.importKeepEnable, value) + } + + var previewImageByClick: Boolean + get() = appCtx.getPrefBoolean(PreferKey.previewImageByClick, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.previewImageByClick, value) + } + + var preDownloadNum + get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10) + set(value) { + appCtx.putPrefInt(PreferKey.preDownloadNum, value) + } + + val syncBookProgress get() = appCtx.getPrefBoolean(PreferKey.syncBookProgress, true) + + val syncBookProgressPlus get() = appCtx.getPrefBoolean(PreferKey.syncBookProgressPlus, false) + + val mediaButtonOnExit get() = appCtx.getPrefBoolean("mediaButtonOnExit", true) + + val readAloudByMediaButton + get() = appCtx.getPrefBoolean(PreferKey.readAloudByMediaButton, false) + + val replaceEnableDefault get() = appCtx.getPrefBoolean(PreferKey.replaceEnableDefault, true) + + val webDavDir get() = appCtx.getPrefString(PreferKey.webDavDir, "legado") + + val webDavDeviceName get() = appCtx.getPrefString(PreferKey.webDavDeviceName, Build.MODEL) + + val recordHeapDump get() = appCtx.getPrefBoolean(PreferKey.recordHeapDump, false) + + val loadCoverOnlyWifi get() = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false) + + val showAddToShelfAlert get() = appCtx.getPrefBoolean(PreferKey.showAddToShelfAlert, true) + + val ignoreAudioFocus get() = appCtx.getPrefBoolean(PreferKey.ignoreAudioFocus, false) + + var pauseReadAloudWhilePhoneCalls + get() = appCtx.getPrefBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, false) + set(value) = appCtx.putPrefBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value) + + val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true) + + val autoCheckNewBackup get() = appCtx.getPrefBoolean(PreferKey.autoCheckNewBackup, true) + + val defaultHomePage get() = appCtx.getPrefString(PreferKey.defaultHomePage, "bookshelf") + + val updateToVariant get() = appCtx.getPrefString(PreferKey.updateToVariant, "default_version") + + val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false) + + val doublePageHorizontal: String? + get() = appCtx.getPrefString(PreferKey.doublePageHorizontal) + + val progressBarBehavior: String? + get() = appCtx.getPrefString(PreferKey.progressBarBehavior, "page") + + val keyPageOnLongPress + get() = appCtx.getPrefBoolean(PreferKey.keyPageOnLongPress, false) + + val volumeKeyPage + get() = appCtx.getPrefBoolean(PreferKey.volumeKeyPage, true) + + val volumeKeyPageOnPlay + get() = appCtx.getPrefBoolean(PreferKey.volumeKeyPageOnPlay, true) + + val mouseWheelPage + get() = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true) + + val paddingDisplayCutouts + get() = appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false) + + var searchScope: String + get() = appCtx.getPrefString("searchScope") ?: "" + set(value) { + appCtx.putPrefString("searchScope", value) + } + + var searchGroup: String + get() = appCtx.getPrefString("searchGroup") ?: "" + set(value) { + appCtx.putPrefString("searchGroup", value) + } + + var pageTouchSlop: Int + get() = appCtx.getPrefInt(PreferKey.pageTouchSlop, 0) + set(value) { + appCtx.putPrefInt(PreferKey.pageTouchSlop, value) + } + + var bookshelfSort: Int + get() = appCtx.getPrefInt(PreferKey.bookshelfSort, 0) + set(value) { + appCtx.putPrefInt(PreferKey.bookshelfSort, value) + } + + fun getBookSortByGroupId(groupId: Long): Int { + return appDb.bookGroupDao.getByID(groupId)?.getRealBookSort() + ?: bookshelfSort + } + + private fun getPrefUserAgent(): String { + val ua = appCtx.getPrefString(PreferKey.userAgent) + if (ua.isNullOrBlank()) { + return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + BuildConfig.Cronet_Main_Version + " Safari/537.36" + } + return ua + } + + var bitmapCacheSize: Int + get() = appCtx.getPrefInt(PreferKey.bitmapCacheSize, 50) + set(value) { + appCtx.putPrefInt(PreferKey.bitmapCacheSize, value) + } + + var imageRetainNum: Int + get() = appCtx.getPrefInt(PreferKey.imageRetainNum, 0) + set(value) { + appCtx.putPrefInt(PreferKey.imageRetainNum, value) + } + + var showReadTitleBarAddition: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showReadTitleAddition, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.showReadTitleAddition, value) + } + var readBarStyleFollowPage: Boolean + get() = appCtx.getPrefBoolean(PreferKey.readBarStyleFollowPage, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.readBarStyleFollowPage, value) + } + + var sourceEditMaxLine: Int + get() { + val maxLine = appCtx.getPrefInt(PreferKey.sourceEditMaxLine, Int.MAX_VALUE) + if (maxLine < 10) { + return Int.MAX_VALUE + } + return maxLine + } + set(value) { + appCtx.putPrefInt(PreferKey.sourceEditMaxLine, value) + } + + var audioPlayUseWakeLock: Boolean + get() = appCtx.getPrefBoolean(PreferKey.audioPlayWakeLock) + set(value) { + appCtx.putPrefBoolean(PreferKey.audioPlayWakeLock, value) + } + + var brightnessVwPos: Boolean + get() = appCtx.getPrefBoolean(PreferKey.brightnessVwPos) + set(value) { + appCtx.putPrefBoolean(PreferKey.brightnessVwPos, value) + } + + fun detectClickArea() { + if (clickActionTL * clickActionTC * clickActionTR + * clickActionML * clickActionMC * clickActionMR + * clickActionBL * clickActionBC * clickActionBR != 0 + ) { + appCtx.putPrefInt(PreferKey.clickActionMC, 0) + appCtx.toastOnUi("当前没有配置菜单区域,自动恢复中间区域为菜单.") + } + } + + //跳转到漫画界面不使用富文本模式 + val showMangaUi: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showMangaUi, true) + + //禁用漫画缩放 + var disableMangaScale: Boolean + get() = appCtx.getPrefBoolean(PreferKey.disableMangaScale, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.disableMangaScale, value) + } + + var disableMangaPageAnim: Boolean + get() = appCtx.getPrefBoolean(PreferKey.disableMangaPageAnim, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.disableMangaPageAnim, value) + } + + //漫画预加载数量 + var mangaPreDownloadNum + get() = appCtx.getPrefInt(PreferKey.mangaPreDownloadNum, 10) + set(value) { + appCtx.putPrefInt(PreferKey.mangaPreDownloadNum, value) + } + + //点击翻页 + var disableClickScroll + get() = appCtx.getPrefBoolean(PreferKey.disableClickScroll, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.disableClickScroll, value) + } + + //漫画滚动速度 + var mangaAutoPageSpeed + get() = appCtx.getPrefInt(PreferKey.mangaAutoPageSpeed, 3) + set(value) { + appCtx.putPrefInt(PreferKey.mangaAutoPageSpeed, value) + } + + //漫画页脚配置 + var mangaFooterConfig + get() = appCtx.getPrefString(PreferKey.mangaFooterConfig, "") + set(value) { + appCtx.putPrefString(PreferKey.mangaFooterConfig, value) + } + + //漫画水平滚动 + var enableMangaHorizontalScroll + get() = appCtx.getPrefBoolean(PreferKey.enableMangaHorizontalScroll, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.enableMangaHorizontalScroll, value) + } + + var mangaColorFilter + get() = appCtx.getPrefString(PreferKey.mangaColorFilter, "") + set(value) { + appCtx.putPrefString(PreferKey.mangaColorFilter, value) + } + + //禁用漫画内标题 + var hideMangaTitle + get() = appCtx.getPrefBoolean(PreferKey.hideMangaTitle, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.hideMangaTitle, value) + } + + //开启墨水屏模式 + var enableMangaEInk + get() = appCtx.getPrefBoolean(PreferKey.enableMangaEInk, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.enableMangaEInk, value) + } + + var mangaEInkThreshold + get() = appCtx.getPrefInt(PreferKey.mangaEInkThreshold, 150) + set(value) { + appCtx.putPrefInt(PreferKey.mangaEInkThreshold, value) + } + + var disableHorizontalPageSnap + get() = appCtx.getPrefBoolean(PreferKey.disableHorizontalPageSnap, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.disableHorizontalPageSnap, value) + } + + var enableMangaGray + get() = appCtx.getPrefBoolean(PreferKey.enableMangaGray, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.enableMangaGray, value) + } + + var welcomeImage + get() = appCtx.getPrefString(PreferKey.welcomeImage) + set(value) { + appCtx.putPrefString(PreferKey.welcomeImage, value) + } + + var welcomeShowText + get() = appCtx.getPrefBoolean(PreferKey.welcomeShowText, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.welcomeShowText, value) + } + + var welcomeShowIcon + get() = appCtx.getPrefBoolean(PreferKey.welcomeShowIcon, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.welcomeShowIcon, value) + } + + var welcomeImageDark + get() = appCtx.getPrefString(PreferKey.welcomeImageDark) + set(value) { + appCtx.putPrefString(PreferKey.welcomeImageDark, value) + } + + var welcomeShowTextDark + get() = appCtx.getPrefBoolean(PreferKey.welcomeShowTextDark, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.welcomeShowTextDark, value) + } + + var welcomeShowIconDark + get() = appCtx.getPrefBoolean(PreferKey.welcomeShowIconDark, true) + set(value) { + appCtx.putPrefBoolean(PreferKey.welcomeShowIconDark, value) + } + +} + diff --git a/app/src/main/java/io/legado/app/help/config/LocalConfig.kt b/app/src/main/java/io/legado/app/help/config/LocalConfig.kt new file mode 100755 index 000000000..255cb2ab2 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/LocalConfig.kt @@ -0,0 +1,126 @@ +package io.legado.app.help.config + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import io.legado.app.utils.getBoolean +import io.legado.app.utils.putBoolean +import io.legado.app.utils.putLong +import io.legado.app.utils.putString +import io.legado.app.utils.remove +import splitties.init.appCtx + +@Suppress("ConstPropertyName") +object LocalConfig : SharedPreferences +by appCtx.getSharedPreferences("local", Context.MODE_PRIVATE) { + + private const val versionCodeKey = "appVersionCode" + + /** + * 本地密码,用来对需要备份的敏感信息加密,如 webdav 配置等 + */ + var password: String? + get() = getString("password", null) + set(value) { + if (value != null) { + putString("password", value) + } else { + remove("password") + } + } + + var lastBackup: Long + get() = getLong("lastBackup", 0) + set(value) { + putLong("lastBackup", value) + } + + var privacyPolicyOk: Boolean + get() = getBoolean("privacyPolicyOk") + set(value) { + putBoolean("privacyPolicyOk", value) + } + + val readHelpVersionIsLast: Boolean + get() = isLastVersion(1, "readHelpVersion", "firstRead") + + val backupHelpVersionIsLast: Boolean + get() = isLastVersion(1, "backupHelpVersion", "firstBackup") + + val readMenuHelpVersionIsLast: Boolean + get() = isLastVersion(1, "readMenuHelpVersion", "firstReadMenu") + + val bookSourcesHelpVersionIsLast: Boolean + get() = isLastVersion(1, "bookSourceHelpVersion", "firstOpenBookSources") + + val webDavBookHelpVersionIsLast: Boolean + get() = isLastVersion(1, "webDavBookHelpVersion", "firstOpenWebDavBook") + + val ruleHelpVersionIsLast: Boolean + get() = isLastVersion(1, "ruleHelpVersion") + + val needUpHttpTTS: Boolean + get() = !isLastVersion(6, "httpTtsVersion") + + val needUpTxtTocRule: Boolean + get() = !isLastVersion(3, "txtTocRuleVersion") + + val needUpRssSources: Boolean + get() = !isLastVersion(6, "rssSourceVersion") + + val needUpDictRule: Boolean + get() = !isLastVersion(2, "needUpDictRule") + + var versionCode + get() = getLong(versionCodeKey, 0) + set(value) { + edit { putLong(versionCodeKey, value) } + } + + val isFirstOpenApp: Boolean + get() { + val value = getBoolean("firstOpen", true) + if (value) { + edit { putBoolean("firstOpen", false) } + } + return value + } + + @Suppress("SameParameterValue") + private fun isLastVersion( + lastVersion: Int, + versionKey: String, + firstOpenKey: String? = null + ): Boolean { + var version = getInt(versionKey, 0) + if (version == 0 && firstOpenKey != null) { + if (!getBoolean(firstOpenKey, true)) { + version = 1 + } + } + if (version < lastVersion) { + edit { putInt(versionKey, lastVersion) } + return false + } + return true + } + + var bookInfoDeleteAlert: Boolean + get() = getBoolean("bookInfoDeleteAlert", true) + set(value) { + putBoolean("bookInfoDeleteAlert", value) + } + + var deleteBookOriginal: Boolean + get() = getBoolean("deleteBookOriginal") + set(value) { + putBoolean("deleteBookOriginal", value) + } + + var appCrash: Boolean + get() = getBoolean("appCrash") + set(value) { + putBoolean("appCrash", value) + } + +} diff --git a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt new file mode 100755 index 000000000..f75c22fc8 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt @@ -0,0 +1,756 @@ +package io.legado.app.help.config + +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import androidx.annotation.Keep +import androidx.core.graphics.toColorInt +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.PageAnim +import io.legado.app.constant.PreferKey +import io.legado.app.help.DefaultData +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.BitmapUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.createFolderReplace +import io.legado.app.utils.externalCache +import io.legado.app.utils.externalFiles +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.getFile +import io.legado.app.utils.getMeanColor +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.hexString +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.resizeAndRecycle +import splitties.init.appCtx +import java.io.File + +/** + * 阅读界面配置 + */ +@Suppress("ConstPropertyName") +@Keep +object ReadBookConfig { + const val configFileName = "readConfig.json" + const val shareConfigFileName = "shareReadConfig.json" + val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName) + val shareConfigFilePath = FileUtils.getPath(appCtx.filesDir, shareConfigFileName) + val configList: ArrayList = arrayListOf() + lateinit var shareConfig: Config + var durConfig + get() = getConfig(styleSelect) + set(value) { + configList[styleSelect] = value + if (shareLayout) { + shareConfig = value + } + } + + var isComic: Boolean = false + var bg: Drawable? = null + var bgMeanColor: Int = 0 + val textColor: Int get() = durConfig.curTextColor() + + init { + initConfigs() + initShareConfig() + } + + @Synchronized + fun getConfig(index: Int): Config { + if (configList.size < 5) { + resetAll() + } + return configList.getOrNull(index) ?: configList[0] + } + + fun initConfigs() { + val configFile = File(configFilePath) + var configs: List? = null + if (configFile.exists()) { + try { + val json = configFile.readText() + configs = GSON.fromJsonArray(json).getOrThrow() + } catch (e: Exception) { + AppLog.put("读取排版配置文件出错", e) + } + } + (configs ?: DefaultData.readConfigs).let { + configList.clear() + configList.addAll(it) + } + } + + fun initShareConfig() { + val configFile = File(shareConfigFilePath) + var c: Config? = null + if (configFile.exists()) { + try { + val json = configFile.readText() + c = GSON.fromJsonObject(json).getOrThrow() + } catch (e: Exception) { + e.printOnDebug() + } + } + shareConfig = c ?: configList.getOrNull(5) ?: Config() + } + + fun upBg(width: Int, height: Int) { + val drawable = durConfig.curBgDrawable(width, height) + if (drawable is BitmapDrawable && drawable.bitmap != null) { + bgMeanColor = drawable.bitmap.getMeanColor() + } else if (drawable is ColorDrawable) { + bgMeanColor = drawable.color + } + val tmp = bg + bg = drawable + (tmp as? BitmapDrawable)?.bitmap?.recycle() + } + + fun save() { + Coroutine.async { + synchronized(this) { + GSON.toJson(configList).let { + FileUtils.delete(configFilePath) + FileUtils.createFileIfNotExist(configFilePath).writeText(it) + } + GSON.toJson(shareConfig).let { + FileUtils.delete(shareConfigFilePath) + FileUtils.createFileIfNotExist(shareConfigFilePath).writeText(it) + } + } + } + } + + fun getAllPicBgStr(): ArrayList { + val list = arrayListOf() + configList.forEach { + if (it.bgType == 2) { + list.add(it.bgStr) + } + if (it.bgTypeNight == 2) { + list.add(it.bgStrNight) + } + if (it.bgTypeEInk == 2) { + list.add(it.bgStrEInk) + } + } + return list + } + + fun deleteDur(): Boolean { + if (configList.size > 5) { + val removeIndex = styleSelect + configList.removeAt(removeIndex) + if (removeIndex <= readStyleSelect) { + readStyleSelect -= 1 + } + if (removeIndex <= comicStyleSelect) { + comicStyleSelect -= 1 + } + return true + } + return false + } + + fun clearBgAndCache() { + val bgs = hashSetOf() + configList.forEach { config -> + repeat(3) { + config.getBgPath(it)?.let { path -> + bgs.add(path) + } + } + } + appCtx.externalFiles.getFile("bg").listFiles()?.forEach { + if (!bgs.contains(it.absolutePath)) { + it.delete() + } + } + FileUtils.delete(appCtx.externalCache.getFile("readConfig")) + val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip") + FileUtils.delete(configZipPath) + } + + private fun resetAll() { + DefaultData.readConfigs.let { + configList.clear() + configList.addAll(it) + save() + } + } + + //配置写入读取 + var readBodyToLh = appCtx.getPrefBoolean(PreferKey.readBodyToLh, true) + var autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 10) + set(value) { + field = value + appCtx.putPrefInt(PreferKey.autoReadSpeed, value) + } + var styleSelect: Int + get() = if (isComic) comicStyleSelect else readStyleSelect + set(value) { + if (isComic) { + comicStyleSelect = value + } else { + readStyleSelect = value + } + } + var readStyleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect) + set(value) { + field = value + if (appCtx.getPrefInt(PreferKey.readStyleSelect) != value) { + appCtx.putPrefInt(PreferKey.readStyleSelect, value) + } + } + var comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect, readStyleSelect) + set(value) { + field = value + if (appCtx.getPrefInt(PreferKey.comicStyleSelect) != value) { + appCtx.putPrefInt(PreferKey.comicStyleSelect, value) + } + } + var shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout) + set(value) { + field = value + if (appCtx.getPrefBoolean(PreferKey.shareLayout) != value) { + appCtx.putPrefBoolean(PreferKey.shareLayout, value) + } + } + + /** + * 两端对齐 + */ + val textFullJustify get() = appCtx.getPrefBoolean(PreferKey.textFullJustify, true) + + /** + * 底部对齐 + */ + val textBottomJustify get() = appCtx.getPrefBoolean(PreferKey.textBottomJustify, true) + var hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar) + var hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar) + var useZhLayout = appCtx.getPrefBoolean(PreferKey.useZhLayout) + + val config get() = if (shareLayout) shareConfig else durConfig + + var bgAlpha: Int + get() = config.bgAlpha + set(value) { + config.bgAlpha = value + } + + var pageAnim: Int + get() = config.curPageAnim() + set(@PageAnim.Anim value) { + config.setCurPageAnim(value) + } + + var textFont: String + get() = config.textFont + set(value) { + config.textFont = value + } + + var textBold: Int + get() = config.textBold + set(value) { + config.textBold = value + } + + var textSize: Int + get() = config.textSize + set(value) { + config.textSize = value + } + + var letterSpacing: Float + get() = config.letterSpacing + set(value) { + config.letterSpacing = value + } + + var lineSpacingExtra: Int + get() = config.lineSpacingExtra + set(value) { + config.lineSpacingExtra = value + } + + var paragraphSpacing: Int + get() = config.paragraphSpacing + set(value) { + config.paragraphSpacing = value + } + + /** + * 标题位置 0:居左 1:居中 2:隐藏 + */ + var titleMode: Int + get() = config.titleMode + set(value) { + config.titleMode = value + } + var titleSize: Int + get() = config.titleSize + set(value) { + config.titleSize = value + } + + /** + * 是否标题居中 + */ + val isMiddleTitle get() = titleMode == 1 + + var titleTopSpacing: Int + get() = config.titleTopSpacing + set(value) { + config.titleTopSpacing = value + } + + var titleBottomSpacing: Int + get() = config.titleBottomSpacing + set(value) { + config.titleBottomSpacing = value + } + + var paragraphIndent: String + get() = config.paragraphIndent + set(value) { + config.paragraphIndent = value + } + + var underline: Boolean + get() = config.underline + set(value) { + config.underline = value + } + + var paddingBottom: Int + get() = config.paddingBottom + set(value) { + config.paddingBottom = value + } + + var paddingLeft: Int + get() = config.paddingLeft + set(value) { + config.paddingLeft = value + } + + var paddingRight: Int + get() = config.paddingRight + set(value) { + config.paddingRight = value + } + + var paddingTop: Int + get() = config.paddingTop + set(value) { + config.paddingTop = value + } + + var headerPaddingBottom: Int + get() = config.headerPaddingBottom + set(value) { + config.headerPaddingBottom = value + } + + var headerPaddingLeft: Int + get() = config.headerPaddingLeft + set(value) { + config.headerPaddingLeft = value + } + + var headerPaddingRight: Int + get() = config.headerPaddingRight + set(value) { + config.headerPaddingRight = value + } + + var headerPaddingTop: Int + get() = config.headerPaddingTop + set(value) { + config.headerPaddingTop = value + } + + var footerPaddingBottom: Int + get() = config.footerPaddingBottom + set(value) { + config.footerPaddingBottom = value + } + + var footerPaddingLeft: Int + get() = config.footerPaddingLeft + set(value) { + config.footerPaddingLeft = value + } + + var footerPaddingRight: Int + get() = config.footerPaddingRight + set(value) { + config.footerPaddingRight = value + } + + var footerPaddingTop: Int + get() = config.footerPaddingTop + set(value) { + config.footerPaddingTop = value + } + + var showHeaderLine: Boolean + get() = config.showHeaderLine + set(value) { + config.showHeaderLine = value + } + + var showFooterLine: Boolean + get() = config.showFooterLine + set(value) { + config.showFooterLine = value + } + + fun getExportConfig(): Config { + val exportConfig = durConfig.copy() + if (shareLayout) { + exportConfig.textFont = shareConfig.textFont + exportConfig.textBold = shareConfig.textBold + exportConfig.textSize = shareConfig.textSize + exportConfig.letterSpacing = shareConfig.letterSpacing + exportConfig.lineSpacingExtra = shareConfig.lineSpacingExtra + exportConfig.paragraphSpacing = shareConfig.paragraphSpacing + exportConfig.titleMode = shareConfig.titleMode + exportConfig.titleSize = shareConfig.titleSize + exportConfig.titleTopSpacing = shareConfig.titleTopSpacing + exportConfig.titleBottomSpacing = shareConfig.titleBottomSpacing + exportConfig.paddingBottom = shareConfig.paddingBottom + exportConfig.paddingLeft = shareConfig.paddingLeft + exportConfig.paddingRight = shareConfig.paddingRight + exportConfig.paddingTop = shareConfig.paddingTop + exportConfig.headerPaddingBottom = shareConfig.headerPaddingBottom + exportConfig.headerPaddingLeft = shareConfig.headerPaddingLeft + exportConfig.headerPaddingRight = shareConfig.headerPaddingRight + exportConfig.headerPaddingTop = shareConfig.headerPaddingTop + exportConfig.footerPaddingBottom = shareConfig.footerPaddingBottom + exportConfig.footerPaddingLeft = shareConfig.footerPaddingLeft + exportConfig.footerPaddingRight = shareConfig.footerPaddingRight + exportConfig.footerPaddingTop = shareConfig.footerPaddingTop + exportConfig.showHeaderLine = shareConfig.showHeaderLine + exportConfig.showFooterLine = shareConfig.showFooterLine + exportConfig.tipHeaderLeft = shareConfig.tipHeaderLeft + exportConfig.tipHeaderMiddle = shareConfig.tipHeaderMiddle + exportConfig.tipHeaderRight = shareConfig.tipHeaderRight + exportConfig.tipFooterLeft = shareConfig.tipFooterLeft + exportConfig.tipFooterMiddle = shareConfig.tipFooterMiddle + exportConfig.tipFooterRight = shareConfig.tipFooterRight + exportConfig.tipColor = shareConfig.tipColor + exportConfig.headerMode = shareConfig.headerMode + exportConfig.footerMode = shareConfig.footerMode + } + return exportConfig + } + + fun import(byteArray: ByteArray): Config { + val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip") + FileUtils.delete(configZipPath) + val zipFile = FileUtils.createFileIfNotExist(configZipPath) + zipFile.writeBytes(byteArray) + val configDir = appCtx.externalCache.getFile("readConfig") + configDir.createFolderReplace() + ZipUtils.unZipToPath(zipFile, configDir) + val configFile = configDir.getFile(configFileName) + val config: Config = GSON.fromJsonObject(configFile.readText()).getOrThrow() + if (config.textFont.isNotEmpty()) { + val fontName = config.textFont + val fontPath = + FileUtils.getPath(appCtx.externalFiles, "font", fontName) + val fontFile = configDir.getFile(fontName) + if (fontFile.exists()) { + if (!FileUtils.exist(fontPath)) { + fontFile.copyTo(File(fontPath)) + } + config.textFont = fontPath + } else { + config.textFont = "" + } + } + if (config.bgType == 2) { + val bgName = FileUtils.getName(config.bgStr) + config.bgStr = bgName + val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName) + if (!FileUtils.exist(bgPath)) { + val bgFile = configDir.getFile(bgName) + if (bgFile.exists()) { + bgFile.copyTo(File(bgPath)) + } + } + config.bgStr = bgPath + } else if (config.bgType == 0) { + config.bgStr.toColorInt() + } + if (config.bgTypeNight == 2) { + val bgName = FileUtils.getName(config.bgStrNight) + config.bgStrNight = bgName + val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName) + if (!FileUtils.exist(bgPath)) { + val bgFile = configDir.getFile(bgName) + if (bgFile.exists()) { + bgFile.copyTo(File(bgPath)) + } + } + config.bgStrNight = bgPath + } else if (config.bgTypeNight == 0) { + config.bgStrNight.toColorInt() + } + if (config.bgTypeEInk == 2) { + val bgName = FileUtils.getName(config.bgStrEInk) + config.bgStrEInk = bgName + val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName) + if (!FileUtils.exist(bgPath)) { + val bgFile = configDir.getFile(bgName) + if (bgFile.exists()) { + bgFile.copyTo(File(bgPath)) + } + } + config.bgStrEInk = bgPath + } else if (config.bgTypeEInk == 0) { + config.bgStrEInk.toColorInt() + } + config.curTextColor() + return config + } + + @Keep + data class Config( + var name: String = "", + var bgStr: String = "#EEEEEE",//白天背景 + var bgStrNight: String = "#000000",//夜间背景 + var bgStrEInk: String = "#FFFFFF",//EInk背景 + var bgAlpha: Int = 100,//背景透明度 + var bgType: Int = 0,//白天背景类型 0:颜色, 1:assets图片, 2其它图片 + var bgTypeNight: Int = 0,//夜间背景类型 + var bgTypeEInk: Int = 0,//EInk背景类型 + private var darkStatusIcon: Boolean = true,//白天是否暗色状态栏 + private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 + private var darkStatusIconEInk: Boolean = true, + private var textColor: String = "#3E3D3B",//白天文字颜色 + private var textColorNight: String = "#ADADAD",//夜间文字颜色 + private var textColorEInk: String = "#000000", + private var pageAnim: Int = 0,//翻页动画 + private var pageAnimEInk: Int = 4, + var textFont: String = "",//字体 + var textBold: Int = 0,//是否粗体字 0:正常, 1:粗体, 2:细体 + var textSize: Int = 20,//文字大小 + var letterSpacing: Float = 0.1f,//字间距 + var lineSpacingExtra: Int = 12,//行间距 + var paragraphSpacing: Int = 2,//段距 + var titleMode: Int = 0,//标题位置 0:居左 1:居中 2:隐藏 + var titleSize: Int = 0, + var titleTopSpacing: Int = 0, + var titleBottomSpacing: Int = 0, + var paragraphIndent: String = "  ",//段落缩进 + var underline: Boolean = false, //下划线 + var paddingBottom: Int = 6, + var paddingLeft: Int = 16, + var paddingRight: Int = 16, + var paddingTop: Int = 6, + var headerPaddingBottom: Int = 0, + var headerPaddingLeft: Int = 16, + var headerPaddingRight: Int = 16, + var headerPaddingTop: Int = 0, + var footerPaddingBottom: Int = 6, + var footerPaddingLeft: Int = 16, + var footerPaddingRight: Int = 16, + var footerPaddingTop: Int = 6, + var showHeaderLine: Boolean = false, + var showFooterLine: Boolean = true, + var tipHeaderLeft: Int = ReadTipConfig.time, + var tipHeaderMiddle: Int = ReadTipConfig.none, + var tipHeaderRight: Int = ReadTipConfig.battery, + var tipFooterLeft: Int = ReadTipConfig.chapterTitle, + var tipFooterMiddle: Int = ReadTipConfig.none, + var tipFooterRight: Int = ReadTipConfig.pageAndTotal, + var tipColor: Int = 0, + var tipDividerColor: Int = -1, + var headerMode: Int = 0, + var footerMode: Int = 0 + ) { + + @Transient + private var textColorIntEInk = -1 + + @Transient + private var textColorIntNight = -1 + + @Transient + private var textColorInt = -1 + + @Transient + private var initColorInt = false + + private fun initColorInt() { + textColorIntEInk = Color.parseColor(textColorEInk) + textColorIntNight = Color.parseColor(textColorNight) + textColorInt = Color.parseColor(textColor) + initColorInt = true + } + + fun setCurTextColor(color: Int) { + when { + AppConfig.isEInkMode -> { + textColorEInk = "#${color.hexString}" + textColorIntEInk = color + } + + AppConfig.isNightTheme -> { + textColorNight = "#${color.hexString}" + textColorIntNight = color + } + + else -> { + textColor = "#${color.hexString}" + textColorInt = color + } + } + } + + fun curTextColor(): Int { + if (!initColorInt) { + initColorInt() + } + return when { + AppConfig.isEInkMode -> textColorIntEInk + AppConfig.isNightTheme -> textColorIntNight + else -> textColorInt + } + } + + fun setCurStatusIconDark(isDark: Boolean) { + when { + AppConfig.isEInkMode -> darkStatusIconEInk = isDark + AppConfig.isNightTheme -> darkStatusIconNight = isDark + else -> darkStatusIcon = isDark + } + } + + fun curStatusIconDark(): Boolean { + return when { + AppConfig.isEInkMode -> darkStatusIconEInk + AppConfig.isNightTheme -> darkStatusIconNight + else -> darkStatusIcon + } + } + + fun setCurPageAnim(@PageAnim.Anim anim: Int) { + when { + AppConfig.isEInkMode -> pageAnimEInk = anim + else -> pageAnim = anim + } + } + + fun curPageAnim(): Int { + return when { + AppConfig.isEInkMode -> pageAnimEInk + else -> pageAnim + } + } + + fun setCurBg(bgType: Int, bg: String) { + when { + AppConfig.isEInkMode -> { + bgTypeEInk = bgType + bgStrEInk = bg + } + + AppConfig.isNightTheme -> { + bgTypeNight = bgType + bgStrNight = bg + } + + else -> { + this.bgType = bgType + bgStr = bg + } + } + } + + fun curBgStr(): String { + return when { + AppConfig.isEInkMode -> bgStrEInk + AppConfig.isNightTheme -> bgStrNight + else -> bgStr + } + } + + fun curBgType(): Int { + return when { + AppConfig.isEInkMode -> bgTypeEInk + AppConfig.isNightTheme -> bgTypeNight + else -> bgType + } + } + + fun curBgDrawable(width: Int, height: Int): Drawable { + if (width == 0 || height == 0) { + return ColorDrawable(appCtx.getCompatColor(R.color.background)) + } + var bgDrawable: Drawable? = null + val resources = appCtx.resources + try { + bgDrawable = when (curBgType()) { + 0 -> ColorDrawable(Color.parseColor(curBgStr())) + 1 -> { + val path = "bg" + File.separator + curBgStr() + val bitmap = BitmapUtils.decodeAssetsBitmap(appCtx, path, width, height) + BitmapDrawable(resources, bitmap?.resizeAndRecycle(width, height)) + } + + else -> { + val path = curBgStr().let { + if (it.contains(File.separator)) it + else FileUtils.getPath(appCtx.externalFiles, "bg", curBgStr()) + } + val bitmap = BitmapUtils.decodeBitmap(path, width, height) + BitmapDrawable(resources, bitmap?.resizeAndRecycle(width, height)) + } + } + } catch (e: OutOfMemoryError) { + e.printOnDebug() + } catch (e: Exception) { + e.printOnDebug() + } + return bgDrawable ?: ColorDrawable(appCtx.getCompatColor(R.color.background)) + } + + fun getBgPath(bgIndex: Int): String? { + val bgType = when (bgIndex) { + 0 -> bgType + 1 -> bgTypeNight + 2 -> bgTypeEInk + else -> error("unknown bgIndex: $bgIndex") + } + if (bgType != 2) { + return null + } + val bgStr = when (bgIndex) { + 0 -> bgStr + 1 -> bgStrNight + 2 -> bgStrEInk + else -> error("unknown bgIndex: $bgIndex") + } + val path = if (bgStr.contains(File.separator)) { + bgStr + } else { + FileUtils.getPath(appCtx.externalFiles, "bg", bgStr) + } + return path + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/config/ReadTipConfig.kt b/app/src/main/java/io/legado/app/help/config/ReadTipConfig.kt new file mode 100755 index 000000000..dc36ffae0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/ReadTipConfig.kt @@ -0,0 +1,107 @@ +package io.legado.app.help.config + +import android.content.Context +import io.legado.app.R +import splitties.init.appCtx + +@Suppress("ConstPropertyName") +object ReadTipConfig { + + const val none = 0 + const val chapterTitle = 1 + const val time = 2 + const val battery = 3 + const val batteryPercentage = 10 + const val page = 4 + const val totalProgress = 5 + const val pageAndTotal = 6 + const val bookName = 7 + const val timeBattery = 8 + const val timeBatteryPercentage = 9 + const val totalProgress1 = 11 + + val tipValues = arrayOf( + none, bookName, chapterTitle, time, battery, batteryPercentage, page, + totalProgress, totalProgress1, pageAndTotal, timeBattery, timeBatteryPercentage + ) + val tipNames get() = appCtx.resources.getStringArray(R.array.read_tip).toList() + + val tipColorNames get() = appCtx.resources.getStringArray(R.array.tip_color).toList() + val tipDividerColorNames + get() = appCtx.resources.getStringArray(R.array.tip_divider_color).toList() + + var tipHeaderLeft: Int + get() = ReadBookConfig.config.tipHeaderLeft + set(value) { + ReadBookConfig.config.tipHeaderLeft = value + } + + var tipHeaderMiddle: Int + get() = ReadBookConfig.config.tipHeaderMiddle + set(value) { + ReadBookConfig.config.tipHeaderMiddle = value + } + + var tipHeaderRight: Int + get() = ReadBookConfig.config.tipHeaderRight + set(value) { + ReadBookConfig.config.tipHeaderRight = value + } + + var tipFooterLeft: Int + get() = ReadBookConfig.config.tipFooterLeft + set(value) { + ReadBookConfig.config.tipFooterLeft = value + } + + var tipFooterMiddle: Int + get() = ReadBookConfig.config.tipFooterMiddle + set(value) { + ReadBookConfig.config.tipFooterMiddle = value + } + + var tipFooterRight: Int + get() = ReadBookConfig.config.tipFooterRight + set(value) { + ReadBookConfig.config.tipFooterRight = value + } + + var headerMode: Int + get() = ReadBookConfig.config.headerMode + set(value) { + ReadBookConfig.config.headerMode = value + } + + var footerMode: Int + get() = ReadBookConfig.config.footerMode + set(value) { + ReadBookConfig.config.footerMode = value + } + + var tipColor: Int + get() = ReadBookConfig.config.tipColor + set(value) { + ReadBookConfig.config.tipColor = value + } + + var tipDividerColor: Int + get() = ReadBookConfig.config.tipDividerColor + set(value) { + ReadBookConfig.config.tipDividerColor = value + } + + fun getHeaderModes(context: Context): LinkedHashMap { + return linkedMapOf( + Pair(0, context.getString(R.string.hide_when_status_bar_show)), + Pair(1, context.getString(R.string.show)), + Pair(2, context.getString(R.string.hide)) + ) + } + + fun getFooterModes(context: Context): LinkedHashMap { + return linkedMapOf( + Pair(0, context.getString(R.string.show)), + Pair(1, context.getString(R.string.hide)) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/config/SourceConfig.kt b/app/src/main/java/io/legado/app/help/config/SourceConfig.kt new file mode 100755 index 000000000..67368309c --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/SourceConfig.kt @@ -0,0 +1,45 @@ +package io.legado.app.help.config + +import android.content.Context.MODE_PRIVATE +import androidx.core.content.edit +import splitties.init.appCtx + +object SourceConfig { + private val sp = appCtx.getSharedPreferences("SourceConfig", MODE_PRIVATE) + fun setBookScore(origin: String, name: String, author: String, score: Int) { + sp.edit { + val preScore = getBookScore(origin, name, author) + var newScore = score + if (preScore != 0) { + newScore = score - preScore + } + + putInt(origin, getSourceScore(origin) + newScore) + + putInt("${origin}_${name}_${author}", score) + } + } + + fun getBookScore(origin: String, name: String, author: String): Int { + return sp.getInt("${origin}_${name}_${author}", 0) + } + + fun getSourceScore(origin: String): Int { + return sp.getInt(origin, 0) + } + + + fun removeSource(origin: String) { + sp.all.keys.filter { + it.startsWith(origin) + }.let { + sp.edit { + it.forEach { + remove(it) + } + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt b/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt new file mode 100755 index 000000000..d8c9b1b37 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt @@ -0,0 +1,341 @@ +package io.legado.app.help.config + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.util.DisplayMetrics +import androidx.annotation.Keep +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.graphics.toColorInt +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.constant.Theme +import io.legado.app.help.DefaultData +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.model.BookCover +import io.legado.app.utils.BitmapUtils +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.externalFiles +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.getFile +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.getPrefString +import io.legado.app.utils.hexString +import io.legado.app.utils.postEvent +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.stackBlur +import splitties.init.appCtx +import java.io.File + +@Keep +object ThemeConfig { + const val configFileName = "themeConfig.json" + val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName) + + val configList: ArrayList by lazy { + val cList = getConfigs() ?: DefaultData.themeConfigs + ArrayList(cList) + } + + fun getTheme() = when { + AppConfig.isEInkMode -> Theme.EInk + AppConfig.isNightTheme -> Theme.Dark + else -> Theme.Light + } + + fun isDarkTheme(): Boolean { + return getTheme() == Theme.Dark + } + + fun applyDayNight(context: Context) { + applyTheme(context) + initNightMode() + BookCover.upDefaultCover() + postEvent(EventBus.RECREATE, "") + } + + fun applyDayNightInit(context: Context) { + applyTheme(context) + initNightMode() + } + + private fun initNightMode() { + val targetMode = + if (AppConfig.isNightTheme) { + AppCompatDelegate.MODE_NIGHT_YES + } else { + AppCompatDelegate.MODE_NIGHT_NO + } + AppCompatDelegate.setDefaultNightMode(targetMode) + } + + fun getBgImage(context: Context, metrics: DisplayMetrics): Bitmap? { + val bgCfg = when (getTheme()) { + Theme.Light -> Pair( + context.getPrefString(PreferKey.bgImage), + context.getPrefInt(PreferKey.bgImageBlurring, 0) + ) + + Theme.Dark -> Pair( + context.getPrefString(PreferKey.bgImageN), + context.getPrefInt(PreferKey.bgImageNBlurring, 0) + ) + + else -> null + } ?: return null + if (bgCfg.first.isNullOrBlank()) return null + val bgImage = BitmapUtils + .decodeBitmap(bgCfg.first!!, metrics.widthPixels, metrics.heightPixels) + if (bgCfg.second == 0) { + return bgImage + } + return bgImage?.stackBlur(bgCfg.second) + } + + fun upConfig() { + getConfigs()?.forEach { config -> + addConfig(config) + } + } + + fun save() { + val json = GSON.toJson(configList) + FileUtils.delete(configFilePath) + FileUtils.createFileIfNotExist(configFilePath).writeText(json) + } + + fun delConfig(index: Int) { + configList.removeAt(index) + save() + } + + fun addConfig(json: String): Boolean { + GSON.fromJsonObject(json.trim { it < ' ' }).getOrNull() + ?.let { + if (validateConfig(it)) { + addConfig(it) + return true + } + } + return false + } + + fun addConfig(newConfig: Config) { + if (!validateConfig(newConfig)) { + return + } + configList.forEachIndexed { index, config -> + if (newConfig.themeName == config.themeName) { + configList[index] = newConfig + return + } + } + configList.add(newConfig) + save() + } + + private fun validateConfig(config: Config): Boolean { + try { + config.primaryColor.toColorInt() + config.accentColor.toColorInt() + config.backgroundColor.toColorInt() + config.bottomBackground.toColorInt() + return true + } catch (_: Exception) { + return false + } + } + + private fun getConfigs(): List? { + val configFile = File(configFilePath) + if (configFile.exists()) { + kotlin.runCatching { + val json = configFile.readText() + return GSON.fromJsonArray(json).getOrThrow() + }.onFailure { + it.printOnDebug() + } + } + return null + } + + fun applyConfig(context: Context, config: Config) { + try { + val primary = Color.parseColor(config.primaryColor) + val accent = Color.parseColor(config.accentColor) + val background = Color.parseColor(config.backgroundColor) + val bBackground = Color.parseColor(config.bottomBackground) + if (config.isNightTheme) { + context.putPrefInt(PreferKey.cNPrimary, primary) + context.putPrefInt(PreferKey.cNAccent, accent) + context.putPrefInt(PreferKey.cNBackground, background) + context.putPrefInt(PreferKey.cNBBackground, bBackground) + } else { + context.putPrefInt(PreferKey.cPrimary, primary) + context.putPrefInt(PreferKey.cAccent, accent) + context.putPrefInt(PreferKey.cBackground, background) + context.putPrefInt(PreferKey.cBBackground, bBackground) + } + AppConfig.isNightTheme = config.isNightTheme + applyDayNight(context) + } catch (e: Exception) { + AppLog.put("设置主题出错\n$e", e, true) + } + } + + fun saveDayTheme(context: Context, name: String) { + val primary = + context.getPrefInt(PreferKey.cPrimary, context.getCompatColor(R.color.md_brown_500)) + val accent = + context.getPrefInt(PreferKey.cAccent, context.getCompatColor(R.color.md_red_600)) + val background = + context.getPrefInt(PreferKey.cBackground, context.getCompatColor(R.color.md_grey_100)) + val bBackground = + context.getPrefInt(PreferKey.cBBackground, context.getCompatColor(R.color.md_grey_200)) + val config = Config( + themeName = name, + isNightTheme = false, + primaryColor = "#${primary.hexString}", + accentColor = "#${accent.hexString}", + backgroundColor = "#${background.hexString}", + bottomBackground = "#${bBackground.hexString}" + ) + addConfig(config) + } + + fun saveNightTheme(context: Context, name: String) { + val primary = + context.getPrefInt( + PreferKey.cNPrimary, + context.getCompatColor(R.color.md_blue_grey_600) + ) + val accent = + context.getPrefInt( + PreferKey.cNAccent, + context.getCompatColor(R.color.md_deep_orange_800) + ) + val background = + context.getPrefInt(PreferKey.cNBackground, context.getCompatColor(R.color.md_grey_900)) + val bBackground = + context.getPrefInt(PreferKey.cNBBackground, context.getCompatColor(R.color.md_grey_850)) + val config = Config( + themeName = name, + isNightTheme = true, + primaryColor = "#${primary.hexString}", + accentColor = "#${accent.hexString}", + backgroundColor = "#${background.hexString}", + bottomBackground = "#${bBackground.hexString}" + ) + addConfig(config) + } + + /** + * 更新主题 + */ + fun applyTheme(context: Context) = with(context) { + when { + AppConfig.isEInkMode -> { + ThemeStore.editTheme(this) + .primaryColor(Color.WHITE) + .accentColor(Color.BLACK) + .backgroundColor(Color.WHITE) + .bottomBackground(Color.WHITE) + .apply() + } + + AppConfig.isNightTheme -> { + val primary = + getPrefInt(PreferKey.cNPrimary, getCompatColor(R.color.md_blue_grey_600)) + val accent = + getPrefInt(PreferKey.cNAccent, getCompatColor(R.color.md_deep_orange_800)) + var background = + getPrefInt(PreferKey.cNBackground, getCompatColor(R.color.md_grey_900)) + if (ColorUtils.isColorLight(background)) { + background = getCompatColor(R.color.md_grey_900) + putPrefInt(PreferKey.cNBackground, background) + } + val bBackground = + getPrefInt(PreferKey.cNBBackground, getCompatColor(R.color.md_grey_850)) + ThemeStore.editTheme(this) + .primaryColor(ColorUtils.withAlpha(primary, 1f)) + .accentColor(ColorUtils.withAlpha(accent, 1f)) + .backgroundColor(ColorUtils.withAlpha(background, 1f)) + .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) + .apply() + } + + else -> { + val primary = + getPrefInt(PreferKey.cPrimary, getCompatColor(R.color.md_brown_500)) + val accent = + getPrefInt(PreferKey.cAccent, getCompatColor(R.color.md_red_600)) + var background = + getPrefInt(PreferKey.cBackground, getCompatColor(R.color.md_grey_100)) + if (!ColorUtils.isColorLight(background)) { + background = getCompatColor(R.color.md_grey_100) + putPrefInt(PreferKey.cBackground, background) + } + val bBackground = + getPrefInt(PreferKey.cBBackground, getCompatColor(R.color.md_grey_200)) + ThemeStore.editTheme(this) + .primaryColor(ColorUtils.withAlpha(primary, 1f)) + .accentColor(ColorUtils.withAlpha(accent, 1f)) + .backgroundColor(ColorUtils.withAlpha(background, 1f)) + .bottomBackground(ColorUtils.withAlpha(bBackground, 1f)) + .apply() + } + } + } + + fun clearBg() { + val bgImagePath = appCtx.getPrefString(PreferKey.bgImage) + appCtx.externalFiles.getFile(PreferKey.bgImage).listFiles()?.forEach { + if (it.absolutePath != bgImagePath) { + it.delete() + } + } + val bgImageNPath = appCtx.getPrefString(PreferKey.bgImageN) + appCtx.externalFiles.getFile(PreferKey.bgImageN).listFiles()?.forEach { + if (it.absolutePath != bgImageNPath) { + it.delete() + } + } + } + + @Keep + data class Config( + var themeName: String, + var isNightTheme: Boolean, + var primaryColor: String, + var accentColor: String, + var backgroundColor: String, + var bottomBackground: String + ) { + + override fun hashCode(): Int { + return GSON.toJson(this).hashCode() + } + + override fun equals(other: Any?): Boolean { + other ?: return false + if (other is Config) { + return other.themeName == themeName + && other.isNightTheme == isNightTheme + && other.primaryColor == primaryColor + && other.accentColor == accentColor + && other.backgroundColor == backgroundColor + && other.bottomBackground == bottomBackground + } + return false + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/coroutine/ActivelyCancelException.kt b/app/src/main/java/io/legado/app/help/coroutine/ActivelyCancelException.kt new file mode 100755 index 000000000..626a866b7 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/coroutine/ActivelyCancelException.kt @@ -0,0 +1,12 @@ +package io.legado.app.help.coroutine + +import kotlin.coroutines.cancellation.CancellationException + +class ActivelyCancelException : CancellationException() { + + override fun fillInStackTrace(): Throwable { + stackTrace = emptyArray() + return this + } + +} diff --git a/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt new file mode 100755 index 000000000..14c123378 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt @@ -0,0 +1,84 @@ +package io.legado.app.help.coroutine + +@Suppress("unused") +class CompositeCoroutine : CoroutineContainer { + + private var resources: HashSet>? = null + + val size: Int + get() = resources?.size ?: 0 + + val isEmpty: Boolean + get() = size == 0 + + constructor() + + constructor(vararg coroutines: Coroutine<*>) { + this.resources = hashSetOf(*coroutines) + } + + constructor(coroutines: Iterable>) { + this.resources = hashSetOf() + for (d in coroutines) { + this.resources?.add(d) + } + } + + override fun add(coroutine: Coroutine<*>): Boolean { + synchronized(this) { + var set: HashSet>? = resources + if (resources == null) { + set = hashSetOf() + resources = set + } + return set!!.add(coroutine) + } + } + + override fun addAll(vararg coroutines: Coroutine<*>): Boolean { + synchronized(this) { + var set: HashSet>? = resources + if (resources == null) { + set = hashSetOf() + resources = set + } + for (coroutine in coroutines) { + val add = set!!.add(coroutine) + if (!add) { + return false + } + } + } + return true + } + + override fun remove(coroutine: Coroutine<*>): Boolean { + if (delete(coroutine)) { + coroutine.cancel() + return true + } + return false + } + + override fun delete(coroutine: Coroutine<*>): Boolean { + synchronized(this) { + val set = resources + if (set == null || !set.remove(coroutine)) { + return false + } + } + return true + } + + override fun clear() { + val set: HashSet>? + synchronized(this) { + set = resources + resources = null + } + + set?.forEachIndexed { _, coroutine -> + coroutine.cancel() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt new file mode 100755 index 000000000..130910166 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt @@ -0,0 +1,252 @@ +package io.legado.app.help.coroutine + +import io.legado.app.utils.printOnDebug +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.coroutines.CoroutineContext + +/** + * 链式协程 + * 注意:如果协程太快完成,回调会不执行 + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +class Coroutine( + private val scope: CoroutineScope, + context: CoroutineContext = Dispatchers.IO, + private val startOption: CoroutineStart = CoroutineStart.DEFAULT, + private val executeContext: CoroutineContext = Dispatchers.Main, + private val semaphore: Semaphore? = null, + block: suspend CoroutineScope.() -> T +) { + + companion object { + + private val DEFAULT = MainScope() + + fun async( + scope: CoroutineScope = DEFAULT, + context: CoroutineContext = Dispatchers.IO, + start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, + semaphore: Semaphore? = null, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine(scope, context, start, executeContext, semaphore, block) + } + + } + + private val job: Job + + private var start: VoidCallback? = null + private var success: Callback? = null + private var error: Callback? = null + private var finally: VoidCallback? = null + private var cancel: VoidCallback? = null + + private var timeMillis: Long? = null + private var errorReturn: Result? = null + + val isCancelled: Boolean + get() = job.isCancelled + + val isActive: Boolean + get() = job.isActive + + val isCompleted: Boolean + get() = job.isCompleted + + init { + this.job = executeInternal(context, block) + } + + fun timeout(timeMillis: () -> Long): Coroutine { + this.timeMillis = timeMillis() + return this@Coroutine + } + + fun timeout(timeMillis: Long): Coroutine { + this.timeMillis = timeMillis + return this@Coroutine + } + + fun onErrorReturn(value: () -> T?): Coroutine { + this.errorReturn = Result(value()) + return this@Coroutine + } + + fun onErrorReturn(value: T?): Coroutine { + this.errorReturn = Result(value) + return this@Coroutine + } + + fun onStart( + context: CoroutineContext? = null, + block: (suspend CoroutineScope.() -> Unit) + ): Coroutine { + this.start = VoidCallback(context, block) + return this@Coroutine + } + + fun onSuccess( + context: CoroutineContext? = null, + block: suspend CoroutineScope.(T) -> Unit + ): Coroutine { + this.success = Callback(context, block) + return this@Coroutine + } + + fun onError( + context: CoroutineContext? = null, + block: suspend CoroutineScope.(Throwable) -> Unit + ): Coroutine { + this.error = Callback(context, block) + return this@Coroutine + } + + /** + * 如果协程被取消,不执行 + */ + fun onFinally( + context: CoroutineContext? = null, + block: suspend CoroutineScope.() -> Unit + ): Coroutine { + this.finally = VoidCallback(context, block) + return this@Coroutine + } + + fun onCancel( + context: CoroutineContext? = null, + block: suspend CoroutineScope.() -> Unit + ): Coroutine { + this.cancel = VoidCallback(context, block) + job.invokeOnCompletion { + if (it is CancellationException && it !is ActivelyCancelException) { + cancel() + } + } + return this@Coroutine + } + + //取消当前任务 + fun cancel(cause: ActivelyCancelException = ActivelyCancelException()) { + if (!job.isCancelled) { + job.cancel(cause) + } + cancel?.let { + DEFAULT.launch(executeContext) { + if (null == it.context) { + it.block.invoke(this) + } else { + withContext(it.context) { + it.block.invoke(this) + } + } + } + } + } + + fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle { + return job.invokeOnCompletion(handler) + } + + fun start() { + job.start() + } + + private fun executeInternal( + context: CoroutineContext, + block: suspend CoroutineScope.() -> T + ): Job { + return (scope.plus(executeContext)).launch(start = startOption) { + semaphore?.acquire() + try { + start?.let { dispatchVoidCallback(this, it) } + ensureActive() + val value = executeBlock(this, context, timeMillis ?: 0L, block) + ensureActive() + success?.let { dispatchCallback(this, value, it) } + } catch (e: Throwable) { + e.printOnDebug() + val consume: Boolean = errorReturn?.value?.let { value -> + success?.let { dispatchCallback(this, value, it) } + true + } ?: false + if (!consume) { + error?.let { dispatchCallback(this, e, it) } + } + } finally { + try { + finally?.let { dispatchVoidCallback(this, it) } + } finally { + semaphore?.release() + } + } + } + } + + private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) { + if (null == callback.context) { + callback.block.invoke(scope) + } else { + withContext(callback.context) { + callback.block.invoke(this) + } + } + } + + private suspend inline fun dispatchCallback( + scope: CoroutineScope, + value: R, + callback: Callback + ) { + if (!scope.isActive) return + if (null == callback.context) { + callback.block.invoke(scope, value) + } else { + withContext(callback.context) { + callback.block.invoke(this, value) + } + } + } + + private suspend inline fun executeBlock( + scope: CoroutineScope, + context: CoroutineContext, + timeMillis: Long, + noinline block: suspend CoroutineScope.() -> T + ): T { + return withContext(context) { + if (timeMillis > 0L) withTimeout(timeMillis) { + block() + } else { + block() + } + } + } + + private data class Result(val value: T?) + + private class VoidCallback( + val context: CoroutineContext?, + val block: suspend CoroutineScope.() -> Unit + ) + + private class Callback( + val context: CoroutineContext?, + val block: suspend CoroutineScope.(VALUE) -> Unit + ) +} diff --git a/app/src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt b/app/src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt new file mode 100755 index 000000000..8ef02af1f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt @@ -0,0 +1,15 @@ +package io.legado.app.help.coroutine + +internal interface CoroutineContainer { + + fun add(coroutine: Coroutine<*>): Boolean + + fun addAll(vararg coroutines: Coroutine<*>): Boolean + + fun remove(coroutine: Coroutine<*>): Boolean + + fun delete(coroutine: Coroutine<*>): Boolean + + fun clear() + +} diff --git a/app/src/main/java/io/legado/app/help/crypto/AsymmetricCrypto.kt b/app/src/main/java/io/legado/app/help/crypto/AsymmetricCrypto.kt new file mode 100755 index 000000000..2d9724f8c --- /dev/null +++ b/app/src/main/java/io/legado/app/help/crypto/AsymmetricCrypto.kt @@ -0,0 +1,86 @@ +package io.legado.app.help.crypto + +import androidx.annotation.Keep +import cn.hutool.crypto.KeyUtil +import cn.hutool.crypto.asymmetric.KeyType +import io.legado.app.utils.EncoderUtils +import java.io.InputStream +import cn.hutool.crypto.asymmetric.AsymmetricCrypto as HutoolAsymmetricCrypto + +@Keep +@Suppress("unused") +class AsymmetricCrypto(algorithm: String) : HutoolAsymmetricCrypto(algorithm) { + + @Suppress("MemberVisibilityCanBePrivate") + fun setPrivateKey(key: ByteArray): AsymmetricCrypto { + setPrivateKey( + KeyUtil.generatePrivateKey(this.algorithm, key) + ) + return this + } + + fun setPrivateKey(key: String): AsymmetricCrypto = setPrivateKey(key.encodeToByteArray()) + + @Suppress("MemberVisibilityCanBePrivate") + fun setPublicKey(key: ByteArray): AsymmetricCrypto { + setPublicKey( + KeyUtil.generatePublicKey(this.algorithm, key) + ) + return this + } + + fun setPublicKey(key: String): AsymmetricCrypto = setPublicKey(key.encodeToByteArray()) + + private fun getKeyType(usePublicKey: Boolean? = true): KeyType { + return when (usePublicKey) { + true -> KeyType.PublicKey + else -> KeyType.PrivateKey + } + } + + @JvmOverloads + fun decrypt(data: Any, usePublicKey: Boolean? = true): ByteArray { + return when (data) { + is ByteArray -> decrypt(data, getKeyType(usePublicKey)) + is String -> decrypt(data, getKeyType(usePublicKey)) + is InputStream -> decrypt(data, getKeyType(usePublicKey)) + else -> throw IllegalArgumentException("Unexpected input type") + } + } + + @JvmOverloads + fun decryptStr(data: Any, usePublicKey: Boolean? = true): String { + return when (data) { + is ByteArray -> String(decrypt(data, getKeyType(usePublicKey))) + is String -> decryptStr(data, getKeyType(usePublicKey)) + is InputStream -> String(decrypt(data, getKeyType(usePublicKey))) + else -> throw IllegalArgumentException("Unexpected input type") + } + } + + @JvmOverloads + fun encrypt(data: Any, usePublicKey: Boolean? = true): ByteArray { + return when (data) { + is ByteArray -> encrypt(data, getKeyType(usePublicKey)) + is String -> encrypt(data, getKeyType(usePublicKey)) + is InputStream -> encrypt(data, getKeyType(usePublicKey)) + else -> throw IllegalArgumentException("Unexpected input type") + } + } + + @JvmOverloads + fun encryptHex(data: Any, usePublicKey: Boolean? = true): String { + return when (data) { + is ByteArray -> encryptHex(data, getKeyType(usePublicKey)) + is String -> encryptHex(data, getKeyType(usePublicKey)) + is InputStream -> encryptHex(data, getKeyType(usePublicKey)) + else -> throw IllegalArgumentException("Unexpected input type") + } + } + + @JvmOverloads + fun encryptBase64(data: Any, usePublicKey: Boolean? = true): String { + return EncoderUtils.base64Encode(encrypt(data, usePublicKey)) + } + +} diff --git a/app/src/main/java/io/legado/app/help/crypto/README.md b/app/src/main/java/io/legado/app/help/crypto/README.md new file mode 100755 index 000000000..5d698b19f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/crypto/README.md @@ -0,0 +1,16 @@ +https://github.com/gedoor/legado/pull/2880 + +非对称加密一般只能知道其中一个密钥,而RhinoJs调用java方法不能传入null和KeyType, 因此提供以下重载函数 +```kotlin +fun setPublicKey(key: ByteArray): T +fun setPublicKey(key: String): T +fun setPrivateKey(key: ByteArray): T +fun setPrivateKey(key: String): T + +fun decrypt(data: Any, usePublicKey: Boolean? = true): ByteArray? +fun decryptStr(data: Any, usePublicKey: Boolean? = true): String? + +fun encrypt(data: Any, usePublicKey: Boolean? = true): ByteArray? +fun encryptHex(data: Any, usePublicKey: Boolean? = true): String? +fun encryptBase64(data: Any, usePublicKey: Boolean? = true): String? +``` \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/crypto/Sign.kt b/app/src/main/java/io/legado/app/help/crypto/Sign.kt new file mode 100755 index 000000000..8c671d920 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/crypto/Sign.kt @@ -0,0 +1,26 @@ +package io.legado.app.help.crypto + +import androidx.annotation.Keep +import cn.hutool.crypto.KeyUtil +import cn.hutool.crypto.asymmetric.Sign as HutoolSign + +@Keep +@Suppress("unused") +class Sign(algorithm: String): HutoolSign(algorithm) { + + fun setPrivateKey(key: ByteArray): Sign { + setPrivateKey(KeyUtil.generatePrivateKey(algorithm, key)) + return this + } + + fun setPrivateKey(key: String): Sign = setPrivateKey(key.encodeToByteArray()) + + fun setPublicKey(key: ByteArray): Sign { + setPublicKey(KeyUtil.generatePublicKey(algorithm, key)) + return this + } + + fun setPublicKey(key: String): Sign = setPublicKey(key.encodeToByteArray()) + +} + diff --git a/app/src/main/java/io/legado/app/help/crypto/SymmetricCryptoAndroid.kt b/app/src/main/java/io/legado/app/help/crypto/SymmetricCryptoAndroid.kt new file mode 100755 index 000000000..c98eb68d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/crypto/SymmetricCryptoAndroid.kt @@ -0,0 +1,47 @@ +package io.legado.app.help.crypto + +import androidx.annotation.Keep +import cn.hutool.core.codec.Base64 +import cn.hutool.core.util.HexUtil +import cn.hutool.crypto.symmetric.SymmetricCrypto +import io.legado.app.utils.EncoderUtils +import io.legado.app.utils.isHex +import java.io.InputStream +import java.nio.charset.Charset + +@Keep +class SymmetricCryptoAndroid( + algorithm: String, + key: ByteArray?, +) : SymmetricCrypto(algorithm, key) { + + override fun encryptBase64(data: ByteArray): String { + return EncoderUtils.base64Encode(encrypt(data)) + } + + override fun encryptBase64(data: String, charset: String?): String { + return EncoderUtils.base64Encode(encrypt(data, charset)) + } + + override fun encryptBase64(data: String, charset: Charset?): String { + return EncoderUtils.base64Encode(encrypt(data, charset)) + } + + override fun encryptBase64(data: String): String { + return EncoderUtils.base64Encode(encrypt(data)) + } + + override fun encryptBase64(data: InputStream): String { + return EncoderUtils.base64Encode(encrypt(data)) + } + + override fun decrypt(data: String): ByteArray { + val bytes = if (data.isHex()) { + HexUtil.decodeHex(data) + } else { + Base64.decode(data) + } + return decrypt(bytes) + } + +} diff --git a/app/src/main/java/io/legado/app/help/exoplayer/ExoPlayerHelper.kt b/app/src/main/java/io/legado/app/help/exoplayer/ExoPlayerHelper.kt new file mode 100755 index 000000000..35956f59b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/exoplayer/ExoPlayerHelper.kt @@ -0,0 +1,143 @@ +package io.legado.app.help.exoplayer + +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import androidx.media3.common.MediaItem +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.FileDataSource +import androidx.media3.datasource.ResolvingDataSource +import androidx.media3.datasource.cache.Cache +import androidx.media3.datasource.cache.CacheDataSink +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.extractor.DefaultExtractorsFactory +import com.google.gson.reflect.TypeToken +import io.legado.app.help.http.okHttpClient +import io.legado.app.utils.GSON +import io.legado.app.utils.externalCache +import okhttp3.CacheControl +import splitties.init.appCtx +import java.io.File +import java.util.concurrent.TimeUnit + + +@Suppress("unused") +@SuppressLint("UnsafeOptInUsageError") +object ExoPlayerHelper { + + private const val SPLIT_TAG = "\uD83D\uDEA7" + + private val mapType by lazy { + object : TypeToken>() {}.type + } + + fun createMediaItem(url: String, headers: Map): MediaItem { + val formatUrl = url + SPLIT_TAG + GSON.toJson(headers, mapType) + return MediaItem.Builder().setUri(formatUrl).build() + } + + fun createHttpExoPlayer(context: Context): ExoPlayer { + return ExoPlayer.Builder(context).setLoadControl( + DefaultLoadControl.Builder().setBufferDurationsMs( + DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, + DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10 + ).build() + + ).setMediaSourceFactory( + DefaultMediaSourceFactory( + context, + DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true) + ).setDataSourceFactory(resolvingDataSource) + .setLiveTargetOffsetMs(5000) + ).build() + } + + + private val resolvingDataSource: ResolvingDataSource.Factory by lazy { + ResolvingDataSource.Factory(cacheDataSourceFactory) { + var res = it + + if (it.uri.toString().contains(SPLIT_TAG)) { + val urls = it.uri.toString().split(SPLIT_TAG) + val url = urls[0] + res = res.withUri(Uri.parse(url)) + try { + val headers: Map = GSON.fromJson(urls[1], mapType) + okhttpDataFactory.setDefaultRequestProperties(headers) + } catch (_: Exception) { + } + } + + res + + } + } + + + /** + * 支持缓存的DataSource.Factory + */ + private val cacheDataSourceFactory by lazy { + //使用自定义的CacheDataSource以支持设置UA + CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(okhttpDataFactory) + .setCacheReadDataSourceFactory(FileDataSource.Factory()) + .setCacheWriteDataSinkFactory( + CacheDataSink.Factory() + .setCache(cache) + .setFragmentSize(CacheDataSink.DEFAULT_FRAGMENT_SIZE) + ) + } + + /** + * Okhttp DataSource.Factory + */ + private val okhttpDataFactory by lazy { + val client = okHttpClient.newBuilder() + .callTimeout(0, TimeUnit.SECONDS) + .build() + OkHttpDataSource.Factory(client) + .setCacheControl(CacheControl.Builder().maxAge(1, TimeUnit.DAYS).build()) + } + + /** + * Exoplayer 内置的缓存 + */ + private val cache: Cache by lazy { + val databaseProvider = StandaloneDatabaseProvider(appCtx) + return@lazy SimpleCache( + //Exoplayer的缓存路径 + File(appCtx.externalCache, "exoplayer"), + //100M的缓存 + LeastRecentlyUsedCacheEvictor((100 * 1024 * 1024).toLong()), + //记录缓存的数据库 + databaseProvider + ) + } + + /** + * 通过kotlin扩展函数+反射实现CacheDataSource.Factory设置默认请求头 + * 需要添加混淆规则 -keepclassmembers class com.google.android.exoplayer2.upstream.cache.CacheDataSource$Factory{upstreamDataSourceFactory;} + * @param headers + * @return + */ +// private fun CacheDataSource.Factory.setDefaultRequestProperties(headers: Map = mapOf()): CacheDataSource.Factory { +// val declaredField = this.javaClass.getDeclaredField("upstreamDataSourceFactory") +// declaredField.isAccessible = true +// val df = declaredField[this] as DataSource.Factory +// if (df is OkHttpDataSource.Factory) { +// df.setDefaultRequestProperties(headers) +// } +// return this +// } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/exoplayer/InputStreamDataSource.kt b/app/src/main/java/io/legado/app/help/exoplayer/InputStreamDataSource.kt new file mode 100755 index 000000000..cc44828b0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/exoplayer/InputStreamDataSource.kt @@ -0,0 +1,81 @@ +package io.legado.app.help.exoplayer + +import android.annotation.SuppressLint +import android.net.Uri +import androidx.media3.common.C +import androidx.media3.datasource.BaseDataSource +import androidx.media3.datasource.DataSpec +import java.io.EOFException +import java.io.IOException +import java.io.InputStream +import kotlin.math.min + + +@SuppressLint("UnsafeOptInUsageError") +class InputStreamDataSource(private val supplier: () -> InputStream) : BaseDataSource(false) { + private var dataSpec: DataSpec? = null + private var bytesRemaining: Long = 0 + private var opened = false + private val inputStream by lazy { + supplier.invoke() + } + + @Throws(IOException::class) + override fun open(dataSpec: DataSpec): Long { + this.dataSpec = dataSpec + transferInitializing(dataSpec) + + inputStream.skip(dataSpec.position) + + bytesRemaining = dataSpec.length + + opened = true + transferStarted(dataSpec) + return bytesRemaining + } + + override fun getUri(): Uri? = dataSpec?.uri + + @Throws(IOException::class) + override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int { + if (readLength == 0) { + return 0 + } else if (bytesRemaining == 0L) { + return C.RESULT_END_OF_INPUT + } + + val bytesToRead = + if (bytesRemaining == C.LENGTH_UNSET.toLong()) readLength + else min(bytesRemaining, readLength.toLong()).toInt() + + val bytesRead = inputStream.read(buffer, offset, bytesToRead) + + if (bytesRead == -1) { + if (bytesRemaining != C.LENGTH_UNSET.toLong()) { + // End of stream reached having not read sufficient data. + throw EOFException() + } + return C.RESULT_END_OF_INPUT + } + + if (bytesRemaining != C.LENGTH_UNSET.toLong()) { + bytesRemaining -= bytesRead.toLong() + bytesTransferred(bytesRead) + } + + return bytesRead + } + + @Throws(IOException::class) + override fun close() { + if (!opened) { + return + } + try { + inputStream.close() + } finally { + opened = false + transferEnded() + } + } +} diff --git a/app/src/main/java/io/legado/app/help/glide/AsyncRecycleBitmapPool.kt b/app/src/main/java/io/legado/app/help/glide/AsyncRecycleBitmapPool.kt new file mode 100755 index 000000000..ebad97b64 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/AsyncRecycleBitmapPool.kt @@ -0,0 +1,25 @@ +package io.legado.app.help.glide + +import android.graphics.Bitmap +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter +import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool +import io.legado.app.help.globalExecutor + +class AsyncRecycleBitmapPool(private val delegate: BitmapPool) : BitmapPool by delegate { + + constructor(maxSize: Int) : this( + if (maxSize > 0) { + LruBitmapPool(maxSize.toLong()) + } else { + BitmapPoolAdapter() + } + ) + + override fun put(bitmap: Bitmap) { + globalExecutor.execute { + delegate.put(bitmap) + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/glide/BlurTransformation.kt b/app/src/main/java/io/legado/app/help/glide/BlurTransformation.kt new file mode 100755 index 000000000..aec2dcaf0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/BlurTransformation.kt @@ -0,0 +1,30 @@ +package io.legado.app.help.glide + +import android.graphics.Bitmap +import androidx.annotation.IntRange +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import io.legado.app.utils.stackBlur +import java.security.MessageDigest + +/** + * 模糊 + * @radius: 0..25 + */ +class BlurTransformation( + @param:IntRange(from = 0, to = 25) private val radius: Int +) : BitmapTransformation() { + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap { + return toTransform.stackBlur(radius) + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update("blur transformation".toByteArray()) + } +} diff --git a/app/src/main/java/io/legado/app/help/glide/FilePathLoader.kt b/app/src/main/java/io/legado/app/help/glide/FilePathLoader.kt new file mode 100755 index 000000000..149020bfd --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/FilePathLoader.kt @@ -0,0 +1,57 @@ +package io.legado.app.help.glide + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.signature.ObjectKey +import java.io.File + +class FilePathLoader : ModelLoader { + override fun buildLoadData( + model: String, + width: Int, + height: Int, + options: com.bumptech.glide.load.Options + ): ModelLoader.LoadData? { + return ModelLoader.LoadData(ObjectKey(model), FilePathFetcher(model)) + } + + override fun handles(model: String): Boolean { + return true + } + + class FilePathFetcher(private val filePath: String) : DataFetcher { + override fun loadData( + priority: Priority, + callback: DataFetcher.DataCallback + ) { + val file = File(filePath) + if (file.exists() && file.isFile) { + callback.onDataReady(file) + } else { + callback.onLoadFailed(Exception("File not found: $filePath")) + } + } + + override fun cleanup() {} + + override fun cancel() {} + + override fun getDataClass(): Class = File::class.java + + override fun getDataSource(): DataSource = DataSource.LOCAL + } + + class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return FilePathLoader() + } + + override fun teardown() {} + } +} + + diff --git a/app/src/main/java/io/legado/app/help/glide/GlideHeaders.kt b/app/src/main/java/io/legado/app/help/glide/GlideHeaders.kt new file mode 100755 index 000000000..a488c61f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/GlideHeaders.kt @@ -0,0 +1,11 @@ +package io.legado.app.help.glide + +import com.bumptech.glide.load.model.Headers + +class GlideHeaders(private val headers: MutableMap) : Headers { + + override fun getHeaders(): MutableMap { + return headers + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/glide/ImageLoader.kt b/app/src/main/java/io/legado/app/help/glide/ImageLoader.kt new file mode 100755 index 000000000..332cc0e5b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/ImageLoader.kt @@ -0,0 +1,108 @@ +package io.legado.app.help.glide + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.annotation.DrawableRes +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.isDataUrl +import io.legado.app.utils.lifecycle +import java.io.File + +//https://bumptech.github.io/glide/doc/generatedapi.html +//Instead of GlideApp, use com.bumptech.Glide +@Suppress("unused") +object ImageLoader { + + /** + * 自动判断path类型 + */ + fun load(context: Context, path: String?): RequestBuilder { + return when { + path.isNullOrEmpty() -> Glide.with(context).load(path) + path.isDataUrl() -> Glide.with(context).load(path) + path.isAbsUrl() -> Glide.with(context).load(path) + path.isContentScheme() -> Glide.with(context).load(Uri.parse(path)) + else -> kotlin.runCatching { + Glide.with(context).load(File(path)) + }.getOrElse { + Glide.with(context).load(path) + } + } + } + + fun load(fragment: Fragment, lifecycle: Lifecycle, path: String?): RequestBuilder { + val requestManager = Glide.with(fragment).lifecycle(lifecycle) + return when { + path.isNullOrEmpty() -> requestManager.load(path) + path.isDataUrl() -> requestManager.load(path) + path.isAbsUrl() -> requestManager.load(path) + path.isContentScheme() -> requestManager.load(Uri.parse(path)) + + else -> kotlin.runCatching { + requestManager.load(File(path)) + }.getOrElse { + requestManager.load(path) + } + } + } + + fun loadBitmap(context: Context, path: String?): RequestBuilder { + val requestManager = Glide.with(context).`as`(Bitmap::class.java) + return when { + path.isNullOrEmpty() -> requestManager.load(path) + path.isDataUrl() -> requestManager.load(path) + path.isAbsUrl() -> requestManager.load(path) + path.isContentScheme() -> requestManager.load(Uri.parse(path)) + else -> kotlin.runCatching { + requestManager.load(File(path)) + }.getOrElse { + requestManager.load(path) + } + } + } + + fun loadFile(context: Context, path: String?): RequestBuilder { + return when { + path.isNullOrEmpty() -> Glide.with(context).asFile().load(path) + path.isAbsUrl() -> Glide.with(context).asFile().load(path) + path.isContentScheme() -> Glide.with(context).asFile().load(Uri.parse(path)) + else -> kotlin.runCatching { + Glide.with(context).asFile().load(File(path)) + }.getOrElse { + Glide.with(context).asFile().load(path) + } + } + } + + fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder { + return Glide.with(context).load(resId) + } + + fun load(context: Context, file: File?): RequestBuilder { + return Glide.with(context).load(file) + } + + fun load(context: Context, uri: Uri?): RequestBuilder { + return Glide.with(context).load(uri) + } + + fun load(context: Context, drawable: Drawable?): RequestBuilder { + return Glide.with(context).load(drawable) + } + + fun load(context: Context, bitmap: Bitmap?): RequestBuilder { + return Glide.with(context).load(bitmap) + } + + fun load(context: Context, bytes: ByteArray?): RequestBuilder { + return Glide.with(context).load(bytes) + } + +} diff --git a/app/src/main/java/io/legado/app/help/glide/LegadoDataUrlLoader.kt b/app/src/main/java/io/legado/app/help/glide/LegadoDataUrlLoader.kt new file mode 100755 index 000000000..a7e14be07 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/LegadoDataUrlLoader.kt @@ -0,0 +1,92 @@ +package io.legado.app.help.glide + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.signature.ObjectKey +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.ReadManga +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.ImageUtils +import com.script.rhino.runScriptWithContext +import kotlinx.coroutines.Job +import java.io.InputStream + +class LegadoDataUrlLoader : ModelLoader { + + override fun buildLoadData( + model: String, + width: Int, + height: Int, + options: Options + ): ModelLoader.LoadData? { + if (options.get(OkHttpModelLoader.mangaOption) == false) { + return null + } + return ModelLoader.LoadData(ObjectKey(model), LegadoDataUrlFetcher(model)) + } + + override fun handles(model: String): Boolean { + return model.startsWith("data:") + } + + class LegadoDataUrlFetcher(private val model: String) : DataFetcher { + + private val coroutineContext = Job() + + override fun loadData( + priority: Priority, + callback: DataFetcher.DataCallback + ) { + try { + val bytes = AnalyzeUrl( + model, source = ReadManga.bookSource, + coroutineContext = coroutineContext + ).getByteArray() + val decoded = runScriptWithContext(coroutineContext) { + ImageUtils.decode( + model, bytes, isCover = false, ReadManga.bookSource, ReadManga.book + )?.inputStream() + } + if (decoded == null) { + throw NoStackTraceException("漫画图片解密失败") + } + callback.onDataReady(decoded) + } catch (e: Exception) { + callback.onLoadFailed(e) + } + } + + override fun cleanup() { + // do nothing + } + + override fun cancel() { + coroutineContext.cancel() + } + + override fun getDataClass(): Class { + return InputStream::class.java + } + + override fun getDataSource(): DataSource { + return DataSource.LOCAL + } + + } + + class Factory : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return LegadoDataUrlLoader() + } + + override fun teardown() { + // do nothing + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt b/app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt new file mode 100755 index 000000000..7d70f6574 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt @@ -0,0 +1,52 @@ +package io.legado.app.help.glide + +import android.content.Context +import android.util.Log +import com.bumptech.glide.Glide +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory +import com.bumptech.glide.load.engine.cache.MemorySizeCalculator +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.module.AppGlideModule +import io.legado.app.BuildConfig +import io.legado.app.help.config.AppConfig +import java.io.File +import java.io.InputStream + + +@Suppress("unused") +@GlideModule +class LegadoGlideModule : AppGlideModule() { + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + registry.replace( + GlideUrl::class.java, + InputStream::class.java, + OkHttpModeLoaderFactory + ) + registry.prepend( + String::class.java, + InputStream::class.java, + LegadoDataUrlLoader.Factory() + ) + registry.prepend( + String::class.java, + File::class.java, + FilePathLoader.Factory() + ) + } + + override fun applyOptions(context: Context, builder: GlideBuilder) { + super.applyOptions(context, builder) + val calculator = MemorySizeCalculator.Builder(context).build() + val bitmapPool = AsyncRecycleBitmapPool(calculator.bitmapPoolSize) + builder.setMemorySizeCalculator(calculator) + builder.setBitmapPool(bitmapPool) + builder.setDiskCache(InternalCacheDiskCacheFactory(context, 1024 * 1024 * 1000)) + if (!BuildConfig.DEBUG && !AppConfig.recordLog) { + builder.setLogLevel(Log.ERROR) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/glide/OkHttpModeLoaderFactory.kt b/app/src/main/java/io/legado/app/help/glide/OkHttpModeLoaderFactory.kt new file mode 100755 index 000000000..6be342699 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/OkHttpModeLoaderFactory.kt @@ -0,0 +1,21 @@ +package io.legado.app.help.glide + +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import okhttp3.Call +import java.io.InputStream + + +object OkHttpModeLoaderFactory: ModelLoaderFactory { + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return OkHttpModelLoader + } + + override fun teardown() { + // Do nothing, this instance doesn't own the client. + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/glide/OkHttpModelLoader.kt b/app/src/main/java/io/legado/app/help/glide/OkHttpModelLoader.kt new file mode 100755 index 000000000..6f8eca83f --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/OkHttpModelLoader.kt @@ -0,0 +1,28 @@ +package io.legado.app.help.glide + +import com.bumptech.glide.load.Option +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.ModelLoader +import java.io.InputStream + +object OkHttpModelLoader : ModelLoader { + + val loadOnlyWifiOption = Option.memory("loadOnlyWifi", false) + val sourceOriginOption = Option.memory("sourceOrigin") + val mangaOption = Option.memory("manga",false) + + override fun buildLoadData( + model: GlideUrl, + width: Int, + height: Int, + options: Options, + ): ModelLoader.LoadData { + return ModelLoader.LoadData(model, OkHttpStreamFetcher(model, options)) + } + + override fun handles(model: GlideUrl): Boolean { + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt b/app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt new file mode 100755 index 000000000..dc50fe4c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt @@ -0,0 +1,164 @@ +package io.legado.app.help.glide + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.HttpException +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.util.ContentLengthInputStream +import com.script.rhino.runScriptWithContext +import io.legado.app.data.entities.BaseSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.addHeaders +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.okHttpClientManga +import io.legado.app.help.source.SourceHelp +import io.legado.app.model.ReadManga +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.ImageUtils +import io.legado.app.utils.isWifiConnect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.SupervisorJob +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import splitties.init.appCtx +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream + + +class OkHttpStreamFetcher( + private val url: GlideUrl, + private val options: Options, +) : + DataFetcher, okhttp3.Callback { + private var stream: InputStream? = null + private var responseBody: ResponseBody? = null + private var callback: DataFetcher.DataCallback? = null + private var source: BaseSource? = null + private val manga = options.get(OkHttpModelLoader.mangaOption) == true + private val coroutineContext = SupervisorJob() + private val coroutineScope = CoroutineScope(coroutineContext) + private lateinit var analyzedUrl: GlideUrl + + @Volatile + private var call: Call? = null + + companion object { + private val failUrl = hashSetOf() + } + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + if (failUrl.contains(url.toStringUrl())) { + callback.onLoadFailed(NoStackTraceException("跳过加载失败的图片")) + return + } + val loadOnlyWifi = options.get(OkHttpModelLoader.loadOnlyWifiOption) ?: false + if (loadOnlyWifi && !appCtx.isWifiConnect) { + callback.onLoadFailed(NoStackTraceException("只在wifi加载图片")) + return + } + + options.get(OkHttpModelLoader.sourceOriginOption)?.let { sourceUrl -> + source = SourceHelp.getSource(sourceUrl) + } + + analyzedUrl = AnalyzeUrl( + url.toString(), + source = source, + coroutineContext = coroutineContext + ).getGlideUrl() + + val requestBuilder = Request.Builder().url(analyzedUrl.toStringUrl()) + requestBuilder.addHeaders(analyzedUrl.headers) + val request: Request = requestBuilder.build() + this.callback = callback + call = if (manga) { + okHttpClientManga.newCall(request) + } else { + okHttpClient.newCall(request) + } + call?.enqueue(this) + } + + override fun cleanup() { + kotlin.runCatching { + stream?.close() + } + responseBody?.close() + coroutineContext.cancel() + callback = null + } + + override fun cancel() { + call?.cancel() + coroutineContext.cancel() + } + + override fun getDataClass(): Class { + return InputStream::class.java + } + + override fun getDataSource(): DataSource { + return DataSource.REMOTE + } + + override fun onFailure(call: Call, e: IOException) { + callback?.onLoadFailed(e) + } + + override fun onResponse(call: Call, response: Response) { + responseBody = response.body + if (!response.isSuccessful) { + if (!manga) { + failUrl.add(url.toStringUrl()) + } + callback?.onLoadFailed(HttpException(response.message, response.code)) + return + } + if (ImageUtils.skipDecode(source, !manga)) { + onStreamReady(responseBody!!.byteStream()) + return + } + Coroutine.async(coroutineScope, executeContext = IO) { + val decodeResult = runScriptWithContext(coroutineContext) { + if (manga) { + ImageUtils.decode( + url.toString(), + responseBody!!.bytes(), + isCover = false, + source, + ReadManga.book + )?.inputStream() + } else { + ImageUtils.decode( + analyzedUrl.toStringUrl(), responseBody!!.byteStream(), + isCover = true, source + ) + } + } + onStreamReady(decodeResult) + } + } + + private fun onStreamReady(inputStream: InputStream?) { + if (inputStream == null) { + if (!manga) { + failUrl.add(url.toStringUrl()) + } + callback?.onLoadFailed(NoStackTraceException("封面二次解密失败")) + } else { + val contentLength: Long = + if (inputStream is ByteArrayInputStream) inputStream.available().toLong() + else responseBody!!.contentLength() + stream = ContentLengthInputStream.obtain(inputStream, contentLength) + callback?.onDataReady(stream) + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/glide/progress/OnProgressListener.kt b/app/src/main/java/io/legado/app/help/glide/progress/OnProgressListener.kt new file mode 100755 index 000000000..f2dcd61a6 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/progress/OnProgressListener.kt @@ -0,0 +1,3 @@ +package io.legado.app.help.glide.progress + +typealias OnProgressListener = (isComplete: Boolean, percentage: Int, bytesRead: Long, totalBytes: Long) -> Unit diff --git a/app/src/main/java/io/legado/app/help/glide/progress/ProgressManager.kt b/app/src/main/java/io/legado/app/help/glide/progress/ProgressManager.kt new file mode 100755 index 000000000..5011b0d2d --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/progress/ProgressManager.kt @@ -0,0 +1,62 @@ +package io.legado.app.help.glide.progress + +import io.legado.app.model.analyzeRule.AnalyzeUrl +import java.util.concurrent.ConcurrentHashMap + +/** + * 进度监听器管理类 + * 加入图片加载进度监听,加入Https支持 + */ +object ProgressManager { + private val listenersMap = ConcurrentHashMap() + + val LISTENER = object : ProgressResponseBody.InternalProgressListener { + override fun onProgress(url: String, bytesRead: Long, totalBytes: Long) { + getProgressListener(url)?.let { + var percentage = (bytesRead * 1f / totalBytes * 100f).toInt() + var isComplete = percentage >= 100 + if (percentage <= -100) { + percentage = 0 + isComplete = true + } + it.invoke(isComplete, percentage, bytesRead, totalBytes) + if (isComplete) { + removeListener(url) + } + } + } + } + + fun addListener(url: String, listener: OnProgressListener) { + if (url.isNotEmpty()) { + val url = getUrlNoOption(url) + listenersMap[url] = listener + listener.invoke(false, 1, 0, 0) + } + } + + fun removeListener(url: String) { + if (url.isNotEmpty()) { + val url = getUrlNoOption(url) + listenersMap.remove(url) + } + } + + fun getProgressListener(url: String): OnProgressListener? { + return if (url.isEmpty() || listenersMap.isEmpty()) { + null + } else { + listenersMap[url] + } + } + + private fun getUrlNoOption(url: String): String { + val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) + return if (urlMatcher.find()) { + url.take(urlMatcher.start()) + } else { + url + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/glide/progress/ProgressResponseBody.kt b/app/src/main/java/io/legado/app/help/glide/progress/ProgressResponseBody.kt new file mode 100755 index 000000000..9a8158a9a --- /dev/null +++ b/app/src/main/java/io/legado/app/help/glide/progress/ProgressResponseBody.kt @@ -0,0 +1,54 @@ +package io.legado.app.help.glide.progress + +import android.os.Handler +import android.os.Looper +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.* +import java.io.IOException +import kotlin.jvm.Throws + +class ProgressResponseBody internal constructor(private val url: String, private val internalProgressListener: InternalProgressListener?, private val responseBody: ResponseBody) : ResponseBody() { + private var bufferedSource: BufferedSource? = null + override fun contentType(): MediaType? { + return responseBody.contentType() + } + + override fun contentLength(): Long { + return responseBody.contentLength() + } + + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + private fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead: Long = 0 + var lastTotalBytesRead: Long = 0 + + @Throws(IOException::class) + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + totalBytesRead += if (bytesRead == -1L) 0 else bytesRead + if (internalProgressListener != null && lastTotalBytesRead != totalBytesRead) { + lastTotalBytesRead = totalBytesRead + mainThreadHandler.post { internalProgressListener.onProgress(url, totalBytesRead, contentLength()) } + } + return bytesRead + } + } + } + + interface InternalProgressListener { + fun onProgress(url: String, bytesRead: Long, totalBytes: Long) + } + + companion object { + private val mainThreadHandler = Handler(Looper.getMainLooper()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/BackstageWebView.kt b/app/src/main/java/io/legado/app/help/http/BackstageWebView.kt new file mode 100755 index 000000000..e8176e713 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/BackstageWebView.kt @@ -0,0 +1,334 @@ +package io.legado.app.help.http + +import android.annotation.SuppressLint +import android.net.http.SslError +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AndroidRuntimeException +import android.webkit.CookieManager +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import io.legado.app.constant.AppConst +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.runOnUI +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import org.apache.commons.text.StringEscapeUtils +import splitties.init.appCtx +import java.lang.ref.WeakReference +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * 后台webView + */ +class BackstageWebView( + private val url: String? = null, + private val html: String? = null, + private val encode: String? = null, + private val tag: String? = null, + private val headerMap: Map? = null, + private val sourceRegex: String? = null, + private val overrideUrlRegex: String? = null, + private val javaScript: String? = null, + private val delayTime: Long = 0, +) { + + private val mHandler = Handler(Looper.getMainLooper()) + private var callback: Callback? = null + private var mWebView: WebView? = null + + suspend fun getStrResponse(): StrResponse = withTimeout(60000L) { + suspendCancellableCoroutine { block -> + block.invokeOnCancellation { + runOnUI { + destroy() + } + } + callback = object : Callback() { + override fun onResult(response: StrResponse) { + if (!block.isCompleted) { + block.resume(response) + } + } + + override fun onError(error: Throwable) { + if (!block.isCompleted) + block.resumeWithException(error) + } + } + runOnUI { + try { + load() + } catch (error: Throwable) { + block.resumeWithException(error) + } + } + } + } + + private fun getEncoding(): String { + return encode ?: "utf-8" + } + + @Throws(AndroidRuntimeException::class) + private fun load() { + val webView = createWebView() + mWebView = webView + try { + when { + !html.isNullOrEmpty() -> if (url.isNullOrEmpty()) { + webView.loadData(html, "text/html", getEncoding()) + } else { + webView.loadDataWithBaseURL(url, html, "text/html", getEncoding(), url) + } + + else -> if (headerMap == null) { + webView.loadUrl(url!!) + } else { + webView.loadUrl(url!!, headerMap) + } + } + } catch (e: Exception) { + callback?.onError(e) + } + } + + @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") + private fun createWebView(): WebView { + val webView = WebView(appCtx) + val settings = webView.settings + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.blockNetworkImage = true + settings.userAgentString = headerMap?.get(AppConst.UA_NAME) ?: AppConfig.userAgent + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + if (sourceRegex.isNullOrBlank() && overrideUrlRegex.isNullOrBlank()) { + webView.webViewClient = HtmlWebViewClient() + } else { + webView.webViewClient = SnifferWebClient() + } + return webView + } + + private fun destroy() { + mWebView?.destroy() + mWebView = null + } + + private fun getJs(): String { + javaScript?.let { + if (it.isNotEmpty()) { + return it + } + } + return JS + } + + private fun setCookie(url: String) { + tag?.let { + Coroutine.async(executeContext = IO) { + val cookie = CookieManager.getInstance().getCookie(url) + CookieStore.setCookie(it, cookie) + } + } + } + + private inner class HtmlWebViewClient : WebViewClient() { + + private var runnable: EvalJsRunnable? = null + private var isRedirect = false + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + isRedirect = isRedirect || if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + request.isRedirect + } else { + request.url.toString() != view.url + } + return super.shouldOverrideUrlLoading(view, request) + } + + override fun onPageFinished(view: WebView, url: String) { + setCookie(url) + if (runnable == null) { + runnable = EvalJsRunnable(view, url, getJs()) + } + mHandler.removeCallbacks(runnable!!) + mHandler.postDelayed(runnable!!, 1000 + delayTime) + } + + @SuppressLint("WebViewClientOnReceivedSslError") + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + handler?.proceed() + } + + private inner class EvalJsRunnable( + webView: WebView, + private val url: String, + private val mJavaScript: String + ) : Runnable { + var retry = 0 + private val mWebView: WeakReference = WeakReference(webView) + override fun run() { + mWebView.get()?.evaluateJavascript(mJavaScript) { + handleResult(it) + } + } + + private fun handleResult(result: String) = Coroutine.async { + if (result.isNotEmpty() && result != "null") { + val content = StringEscapeUtils.unescapeJson(result) + .replace(quoteRegex, "") + try { + val response = buildStrResponse(content) + callback?.onResult(response) + } catch (e: Exception) { + callback?.onError(e) + } + mHandler.post { + destroy() + } + return@async + } + if (retry > 30) { + callback?.onError(NoStackTraceException("js执行超时")) + mHandler.post { + destroy() + } + return@async + } + retry++ + mHandler.postDelayed(this@EvalJsRunnable, 1000) + } + + private fun buildStrResponse(content: String): StrResponse { + if (!isRedirect) { + return StrResponse(url, content) + } + val originUrl = this@BackstageWebView.url ?: url + val originResponse = Response.Builder() + .code(302) + .request(Request.Builder().url(originUrl).build()) + .protocol(Protocol.HTTP_1_1) + .message("Found") + .build() + val response = Response.Builder() + .code(200) + .request(Request.Builder().url(url).build()) + .protocol(Protocol.HTTP_1_1) + .message("OK") + .priorResponse(originResponse) + .build() + return StrResponse(response, content) + } + } + + } + + private inner class SnifferWebClient : WebViewClient() { + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + if (shouldOverrideUrlLoading(request.url.toString())) { + return true + } + return super.shouldOverrideUrlLoading(view, request) + } + + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "KotlinRedundantDiagnosticSuppress") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + if (shouldOverrideUrlLoading(url)) { + return true + } + return super.shouldOverrideUrlLoading(view, url) + } + + private fun shouldOverrideUrlLoading(requestUrl: String): Boolean { + overrideUrlRegex?.let { + if (requestUrl.matches(it.toRegex())) { + try { + val response = StrResponse(url!!, requestUrl) + callback?.onResult(response) + } catch (e: Exception) { + callback?.onError(e) + } + destroy() + return true + } + } + return false + } + + override fun onLoadResource(view: WebView, resUrl: String) { + sourceRegex?.let { + if (resUrl.matches(it.toRegex())) { + try { + val response = StrResponse(url!!, resUrl) + callback?.onResult(response) + } catch (e: Exception) { + callback?.onError(e) + } + destroy() + } + } + } + + override fun onPageFinished(webView: WebView, url: String) { + setCookie(url) + if (!javaScript.isNullOrEmpty()) { + val runnable = LoadJsRunnable(webView, javaScript) + mHandler.postDelayed(runnable, 1000L + delayTime) + } + } + + @SuppressLint("WebViewClientOnReceivedSslError") + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + handler?.proceed() + } + + private inner class LoadJsRunnable( + webView: WebView, + private val mJavaScript: String? + ) : Runnable { + private val mWebView: WeakReference = WeakReference(webView) + override fun run() { + mWebView.get()?.loadUrl("javascript:${mJavaScript}") + } + } + + } + + companion object { + const val JS = "document.documentElement.outerHTML" + private val quoteRegex = "^\"|\"$".toRegex() + } + + abstract class Callback { + abstract fun onResult(response: StrResponse) + abstract fun onError(error: Throwable) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/CookieManager.kt b/app/src/main/java/io/legado/app/help/http/CookieManager.kt new file mode 100755 index 000000000..5d90e52d0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/CookieManager.kt @@ -0,0 +1,170 @@ +package io.legado.app.help.http + +import android.webkit.CookieManager +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.help.CacheManager +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.splitNotBlank +import okhttp3.Cookie +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Connection + +@Suppress("ConstPropertyName") +object CookieManager { + /** + * _session_cookie 会话期 cookie,应用重启后失效 + * _cookie cookies 缓存 + */ + + const val cookieJarHeader = "CookieJar" + + /** + * 从响应中保存Cookies + */ + fun saveResponse(response: Response) { + val url = response.request.url + val headers = response.headers + saveCookiesFromHeaders(url, headers) + } + + fun saveResponse(response: Connection.Response) { + val url = response.url().toHttpUrlOrNull() ?: return + val headers = response.multiHeaders().toHeaders() + saveCookiesFromHeaders(url, headers) + } + + private fun saveCookiesFromHeaders(url: HttpUrl, headers: Headers) { + val domain = NetworkUtils.getSubDomain(url.toString()) + val cookies = Cookie.parseAll(url, headers) + + val sessionCookie = cookies.filter { !it.persistent }.getString() + updateSessionCookie(domain, sessionCookie) + + val cookieString = cookies.filter { it.persistent }.getString() + CookieStore.replaceCookie(domain, cookieString) + } + + /** + * 加载Cookies到请求中 + */ + fun loadRequest(request: Request): Request { + val url = request.url.toString() + val domain = NetworkUtils.getSubDomain(url) + + val cookie = CookieStore.getCookie(domain) + val requestCookie = request.header("Cookie") + + val newCookie = mergeCookies(requestCookie, cookie) ?: return request + + kotlin.runCatching { + return request.newBuilder() + .header("Cookie", newCookie) + .build() + }.onFailure { + CookieStore.removeCookie(url) + val msg = "设置cookie出错,已清除cookie $domain cookie:$newCookie\n$it" + AppLog.put(msg, it) + } + + return request + } + + private fun getSessionCookieMap(domain: String): MutableMap? { + return getSessionCookie(domain)?.let { CookieStore.cookieToMap(it) } + } + + fun getSessionCookie(domain: String): String? { + return CacheManager.getFromMemory("${domain}_session_cookie") as? String + } + + private fun updateSessionCookie(domain: String, cookies: String) { + val sessionCookie = getSessionCookie(domain) + if (sessionCookie.isNullOrEmpty()) { + CacheManager.putMemory("${domain}_session_cookie", cookies) + return + } + + val ck = mergeCookies(sessionCookie, cookies) ?: return + CacheManager.putMemory("${domain}_session_cookie", ck) + } + + fun mergeCookies(vararg cookies: String?): String? { + val cookieMap = mergeCookiesToMap(*cookies) + return CookieStore.mapToCookie(cookieMap) + } + + fun mergeCookiesToMap(vararg cookies: String?): MutableMap { + return cookies.filterNotNull().map { + CookieStore.cookieToMap(it) + }.reduce { acc, cookieMap -> + acc.apply { putAll(cookieMap) } + } + } + + /** + * 删除单个Cookie + */ + fun removeCookie(url: String, key: String) { + val domain = NetworkUtils.getSubDomain(url) + + getSessionCookieMap(domain)?.let { + it.remove(key) + CookieStore.mapToCookie(it)?.let { cookie -> + CacheManager.putMemory("${domain}_session_cookie", cookie) + } + } + + val cookie = getCookieNoSession(url) + if (cookie.isNotEmpty()) { + val cookieMap = CookieStore.cookieToMap(cookie).apply { remove(key) } + CookieStore.mapToCookie(cookieMap)?.let { + CookieStore.setCookie(url, it) + } + } + } + + fun getCookieNoSession(url: String): String { + val domain = NetworkUtils.getSubDomain(url) + val cacheCookie = CacheManager.getFromMemory("${domain}_cookie") as? String + + return if (cacheCookie != null) { + cacheCookie + } else { + val cookieBean = appDb.cookieDao.get(domain) + cookieBean?.cookie ?: "" + } + } + + fun applyToWebView(url: String) { + val baseUrl = NetworkUtils.getBaseUrl(url) ?: return + val cookies = CookieStore.getCookie(url).splitNotBlank(";") + val cookieManager = CookieManager.getInstance() + cookieManager.removeSessionCookies(null) + cookies.forEach { + cookieManager.setCookie(baseUrl, it) + } + } + + fun List.getString() = buildString { + this@getString.forEachIndexed { index, cookie -> + if (index > 0) append("; ") + append(cookie.name).append('=').append(cookie.value) + } + } + + private fun Map>.toHeaders(): Headers { + return Headers.Builder().apply { + this@toHeaders.forEach { (k, v) -> + v.forEach { + add(k, it) + } + } + }.build() + } + +} diff --git a/app/src/main/java/io/legado/app/help/http/CookieStore.kt b/app/src/main/java/io/legado/app/help/http/CookieStore.kt new file mode 100755 index 000000000..63ee8a943 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/CookieStore.kt @@ -0,0 +1,123 @@ +@file:Suppress("unused") + +package io.legado.app.help.http + +import android.text.TextUtils +import androidx.annotation.Keep +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern.equalsRegex +import io.legado.app.constant.AppPattern.semicolonRegex +import io.legado.app.data.appDb +import io.legado.app.data.entities.Cookie +import io.legado.app.help.CacheManager +import io.legado.app.help.http.CookieManager.getCookieNoSession +import io.legado.app.help.http.CookieManager.mergeCookiesToMap +import io.legado.app.help.http.api.CookieManagerInterface +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.removeCookie + +@Keep +object CookieStore : CookieManagerInterface { + + /** + *保存cookie到数据库,会自动识别url的二级域名 + */ + override fun setCookie(url: String, cookie: String?) { + try { + val domain = NetworkUtils.getSubDomain(url) + CacheManager.putMemory("${domain}_cookie", cookie ?: "") + val cookieBean = Cookie(domain, cookie ?: "") + appDb.cookieDao.insert(cookieBean) + } catch (e: Exception) { + AppLog.put("保存Cookie失败\n$e", e) + } + } + + override fun replaceCookie(url: String, cookie: String) { + if (TextUtils.isEmpty(url) || TextUtils.isEmpty(cookie)) { + return + } + val oldCookie = getCookieNoSession(url) + if (TextUtils.isEmpty(oldCookie)) { + setCookie(url, cookie) + } else { + val cookieMap = cookieToMap(oldCookie) + cookieMap.putAll(cookieToMap(cookie)) + val newCookie = mapToCookie(cookieMap) + setCookie(url, newCookie) + } + } + + /** + *获取url所属的二级域名的cookie + */ + override fun getCookie(url: String): String { + val domain = NetworkUtils.getSubDomain(url) + + val cookie = getCookieNoSession(url) + val sessionCookie = CookieManager.getSessionCookie(domain) + + val cookieMap = mergeCookiesToMap(cookie, sessionCookie) + + var ck = mapToCookie(cookieMap) ?: "" + while (ck.length > 4096) { + val removeKey = cookieMap.keys.random() + CookieManager.removeCookie(url, removeKey) + cookieMap.remove(removeKey) + ck = mapToCookie(cookieMap) ?: "" + } + return ck + } + + fun getKey(url: String, key: String): String { + val cookie = getCookie(url) + val sessionCookie = CookieManager.getSessionCookie(url) + val cookieMap = mergeCookiesToMap(cookie, sessionCookie) + return cookieMap[key] ?: "" + } + + override fun removeCookie(url: String) { + val domain = NetworkUtils.getSubDomain(url) + appDb.cookieDao.delete(domain) + CacheManager.deleteMemory("${domain}_cookie") + CacheManager.deleteMemory("${domain}_session_cookie") + android.webkit.CookieManager.getInstance().removeCookie(url) + } + + override fun cookieToMap(cookie: String): MutableMap { + val cookieMap = mutableMapOf() + if (cookie.isBlank()) { + return cookieMap + } + val pairArray = cookie.split(semicolonRegex).dropLastWhile { it.isEmpty() }.toTypedArray() + for (pair in pairArray) { + val pairs = pair.split(equalsRegex, 2).dropLastWhile { it.isEmpty() }.toTypedArray() + if (pairs.size <= 1) { + continue + } + val key = pairs[0].trim { it <= ' ' } + val value = pairs[1] + if (value.isNotBlank() || value.trim { it <= ' ' } == "null") { + cookieMap[key] = value.trim { it <= ' ' } + } + } + return cookieMap + } + + override fun mapToCookie(cookieMap: Map?): String? { + if (cookieMap.isNullOrEmpty()) { + return null + } + val builder = StringBuilder() + cookieMap.keys.forEachIndexed { index, key -> + if (index > 0) builder.append("; ") + builder.append(key).append("=").append(cookieMap[key]) + } + return builder.toString() + } + + fun clear() { + appDb.cookieDao.deleteOkHttp() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/Cronet.kt b/app/src/main/java/io/legado/app/help/http/Cronet.kt new file mode 100755 index 000000000..36d8e3fcd --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/Cronet.kt @@ -0,0 +1,29 @@ +package io.legado.app.help.http + +import io.legado.app.lib.cronet.CronetInterceptor +import io.legado.app.lib.cronet.CronetLoader +import okhttp3.Interceptor + +object Cronet { + + val loader: LoaderInterface? by lazy { + CronetLoader + } + + fun preDownload() { + loader?.preDownload() + } + + val interceptor: Interceptor? by lazy { + CronetInterceptor(cookieJar) + } + + interface LoaderInterface { + + fun install(): Boolean + + fun preDownload() + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/DecompressInterceptor.kt b/app/src/main/java/io/legado/app/help/http/DecompressInterceptor.kt new file mode 100755 index 000000000..976d01f86 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/DecompressInterceptor.kt @@ -0,0 +1,45 @@ +package io.legado.app.help.http + +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.asResponseBody +import okhttp3.internal.http.promisesBody +import okio.buffer +import okio.source +import java.util.zip.GZIPInputStream +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream + +object DecompressInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val requestBuilder = request.newBuilder() + + var transparentDecompress = false + if (request.header("Accept-Encoding") == null && request.header("Range") == null) { + transparentDecompress = true + requestBuilder.header("Accept-Encoding", "gzip, deflate") + } + + val response = chain.proceed(requestBuilder.build()) + val body = response.body + + if (!transparentDecompress || !response.promisesBody() || body == ResponseBody.EMPTY) { + return response + } + + val encoding = response.header("Content-Encoding")?.lowercase() + val source = when (encoding) { + "gzip" -> GZIPInputStream(body.byteStream()).source().buffer() + "deflate" -> InflaterInputStream(body.byteStream(), Inflater(true)).source().buffer() + else -> return response + } + + return response.newBuilder() + .removeHeader("Content-Encoding") + .removeHeader("Content-Length") + .body(source.asResponseBody(body.contentType(), -1)) + .build() + } +} diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt new file mode 100755 index 000000000..1efd9e8c5 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -0,0 +1,184 @@ +package io.legado.app.help.http + +import io.legado.app.constant.AppConst +import io.legado.app.help.CacheManager +import io.legado.app.help.config.AppConfig +import io.legado.app.help.glide.progress.ProgressManager.LISTENER +import io.legado.app.help.glide.progress.ProgressResponseBody +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.model.ReadManga +import io.legado.app.utils.NetworkUtils +import okhttp3.ConnectionSpec +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.Credentials +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ThreadFactory +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit + +private val proxyClientCache: ConcurrentHashMap by lazy { + ConcurrentHashMap() +} + +val cookieJar by lazy { + object : CookieJar { + + override fun loadForRequest(url: HttpUrl): List { + return emptyList() + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + if (cookies.isEmpty()) return + //临时保存 书源启用cookie选项再添加到数据库 + val cookieBuilder = StringBuilder() + cookies.forEachIndexed { index, cookie -> + if (index > 0) cookieBuilder.append(";") + cookieBuilder.append(cookie.name).append('=').append(cookie.value) + } + val domain = NetworkUtils.getSubDomain(url.toString()) + CacheManager.putMemory("${domain}_cookieJar", cookieBuilder.toString()) + } + + } +} + +val okHttpClient: OkHttpClient by lazy { + val specs = arrayListOf( + ConnectionSpec.MODERN_TLS, + ConnectionSpec.COMPATIBLE_TLS, + ConnectionSpec.CLEARTEXT + ) + + val builder = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(15, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) + //.cookieJar(cookieJar = cookieJar) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory, SSLHelper.unsafeTrustManager) + .retryOnConnectionFailure(true) + .hostnameVerifier(SSLHelper.unsafeHostnameVerifier) + .connectionSpecs(specs) + .followRedirects(true) + .followSslRedirects(true) + .addInterceptor(OkHttpExceptionInterceptor) + .addInterceptor { chain -> + val request = chain.request() + val builder = request.newBuilder() + if (request.header(AppConst.UA_NAME) == null) { + builder.addHeader(AppConst.UA_NAME, AppConfig.userAgent) + } else if (request.header(AppConst.UA_NAME) == "null") { + builder.removeHeader(AppConst.UA_NAME) + } + builder.addHeader("Keep-Alive", "300") + builder.addHeader("Connection", "Keep-Alive") + builder.addHeader("Cache-Control", "no-cache") + chain.proceed(builder.build()) + } + .addNetworkInterceptor { chain -> + var request = chain.request() + val enableCookieJar = request.header(cookieJarHeader) != null + + if (enableCookieJar) { + val requestBuilder = request.newBuilder() + requestBuilder.removeHeader(cookieJarHeader) + request = CookieManager.loadRequest(requestBuilder.build()) + } + + val networkResponse = chain.proceed(request) + + if (enableCookieJar) { + CookieManager.saveResponse(networkResponse) + } + networkResponse + } + if (AppConfig.isCronet) { + if (Cronet.loader?.install() == true) { + Cronet.interceptor?.let { + builder.addInterceptor(it) + } + } + } + builder.addInterceptor(DecompressInterceptor) + builder.build().apply { + val okHttpName = + OkHttpClient::class.java.name.removePrefix("okhttp3.").removeSuffix("Client") + val executor = dispatcher.executorService as ThreadPoolExecutor + val threadName = "$okHttpName Dispatcher" + executor.threadFactory = ThreadFactory { runnable -> + Thread(runnable, threadName).apply { + isDaemon = false + uncaughtExceptionHandler = OkhttpUncaughtExceptionHandler + } + } + } +} + +val okHttpClientManga by lazy { + okHttpClient.newBuilder().run { + val interceptors = interceptors() + interceptors.add(1) { chain -> + val request = chain.request() + val response = chain.proceed(request) + val url = request.url.toString() + response.newBuilder() + .body(ProgressResponseBody(url, LISTENER, response.body)) + .build() + } + interceptors.add(1) { chain -> + ReadManga.rateLimiter.withLimitBlocking { + chain.proceed(chain.request()) + } + } + build() + } +} + +/** + * 缓存代理okHttp + */ +fun getProxyClient(proxy: String? = null): OkHttpClient { + if (proxy.isNullOrBlank()) { + return okHttpClient + } + proxyClientCache[proxy]?.let { + return it + } + val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?") + val ms = r.findAll(proxy) + val group = ms.first() + var username = "" //代理服务器验证用户名 + var password = "" //代理服务器验证密码 + val type = if (group.groupValues[1] == "http") "http" else "socks" + val host = group.groupValues[2] + val port = group.groupValues[3].toInt() + if (group.groupValues[4] != "") { + username = group.groupValues[4].split("@")[1] + password = group.groupValues[4].split("@")[2] + } + if (host != "") { + val builder = okHttpClient.newBuilder() + if (type == "http") { + builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))) + } else { + builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port))) + } + if (username != "" && password != "") { + builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码 + val credential: String = Credentials.basic(username, password) + response.request.newBuilder() + .header("Proxy-Authorization", credential) + .build() + } + } + val proxyClient = builder.build() + proxyClientCache[proxy] = proxyClient + return proxyClient + } + return okHttpClient +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt b/app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt new file mode 100755 index 000000000..f55ca2ed8 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt @@ -0,0 +1,1201 @@ +package io.legado.app.help.http + +import android.os.Build +import androidx.annotation.RequiresApi +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.help.http.SSLHelper.unsafeTrustManager +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Dispatcher +import okhttp3.Handshake +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okio.Buffer +import okio.BufferedSink +import okio.Pipe +import okio.Timeout +import okio.buffer +import java.io.BufferedReader +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.InterruptedIOException +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.InetSocketAddress +import java.net.MalformedURLException +import java.net.ProtocolException +import java.net.Proxy +import java.net.SocketPermission +import java.net.SocketTimeoutException +import java.net.URL +import java.net.URLConnection +import java.net.URLStreamHandler +import java.net.URLStreamHandlerFactory +import java.security.AccessControlException +import java.security.Permission +import java.security.Principal +import java.security.cert.Certificate +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Collections +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.TreeMap +import java.util.concurrent.TimeUnit +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLSocketFactory + +/** + * OkHttp 3.14 dropped support for the long-deprecated OkUrlFactory class, which allows you to use + * the HttpURLConnection API with OkHttp's implementation. This class does the same thing using only + * public APIs in OkHttp. It requires OkHttp 3.14 or newer. + * + * + * Rather than pasting this 1100 line gist into your source code, please upgrade to OkHttp's + * request/response API. Your code will be shorter, easier to read, and you'll be able to use + * interceptors. + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFactory, + Cloneable { + fun client(): OkHttpClient { + return client + } + + fun setClient(client: OkHttpClient): ObsoleteUrlFactory { + this.client = client + return this + } + + /** + * Returns a copy of this stream handler factory that includes a shallow copy of the internal + * [HTTP client][OkHttpClient]. + */ + public override fun clone(): ObsoleteUrlFactory { + return ObsoleteUrlFactory(client) + } + + fun open(url: URL): HttpURLConnection { + return open(url, client.proxy) + } + + fun open(url: URL, proxy: Proxy?): HttpURLConnection { + val protocol = url.protocol + val copy = client.newBuilder() + .proxy(proxy) + .build() + if (protocol == "http") return OkHttpURLConnection(url, copy) + if (protocol == "https") return OkHttpsURLConnection(url, copy) + throw IllegalArgumentException("Unexpected protocol: $protocol") + } + + /** + * Creates a URLStreamHandler as a [java.net.URL.setURLStreamHandlerFactory]. + * + * + * This code configures OkHttp to handle all HTTP and HTTPS connections + * created with [java.net.URL.openConnection]:
       `OkHttpClient okHttpClient = new OkHttpClient();
    +     * URL.setURLStreamHandlerFactory(new ObsoleteUrlFactory(okHttpClient));
    +    `
    * + */ + override fun createURLStreamHandler(protocol: String): URLStreamHandler? { + return if (protocol != "http" && protocol != "https") null else object : + URLStreamHandler() { + override fun openConnection(url: URL): URLConnection { + return open(url) + } + + override fun openConnection(url: URL, proxy: Proxy): URLConnection { + return open(url, proxy) + } + + override fun getDefaultPort(): Int { + if ((protocol == "http")) return 80 + if ((protocol == "https")) return 443 + throw AssertionError() + } + } + } + + internal class OkHttpURLConnection( + url: URL?, // These fields are confined to the application thread that uses HttpURLConnection. + var client: OkHttpClient + ) : + HttpURLConnection(url), Callback { + private val networkInterceptor: NetworkInterceptor = NetworkInterceptor() + var requestHeaders: Headers.Builder = Headers.Builder() + var responseHeaders: Headers? = null + var executed = false + var call: Call? = null + + /** Like the superclass field of the same name, but a long and available on all platforms. */ + //var fixedContentLength = -1L + + // These fields are guarded by lock. + private val lock = Any() + private var response: Response? = null + private var callFailure: Throwable? = null + var networkResponse: Response? = null + var connectPending = true + var proxy: Proxy? = null + var handshake: Handshake? = null + + @Throws(IOException::class) + override fun connect() { + if (executed) return + val call = buildCall() + executed = true + call.enqueue(this) + synchronized(lock) { + try { + while (connectPending && (response == null) && (callFailure == null)) { + lock.wait() // Wait 'til the network interceptor is reached or the call fails. + } + if (callFailure != null) { + throw propagate(callFailure) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException() + } + } + } + + override fun disconnect() { + // Calling disconnect() before a connection exists should have no effect. + if (call == null) return + networkInterceptor.proceed() // Unblock any waiting async thread. + call!!.cancel() + } + + override fun getErrorStream(): InputStream? { + return try { + val response = getResponse(true) + if (hasBody(response) && response.code >= HTTP_BAD_REQUEST) { + response.body.byteStream() + } else null + } catch (e: IOException) { + null + } + } + + @get:Throws(IOException::class) + val headers: Headers + get() { + if (responseHeaders == null) { + val response = getResponse(true) + val headers = response.headers + responseHeaders = headers.newBuilder() + .add(SELECTED_PROTOCOL, response.protocol.toString()) + .add(RESPONSE_SOURCE, responseSourceHeader(response)) + .build() + } + return responseHeaders as Headers + } + + override fun getHeaderField(position: Int): String? { + return try { + val headers = headers + if (position < 0 || position >= headers.size) null + else headers.value(position) + } catch (e: IOException) { + null + } + } + + override fun getHeaderField(fieldName: String?): String? { + return try { + if (fieldName == null) statusLineToString(getResponse(true)) else headers[fieldName] + } catch (e: IOException) { + null + } + } + + override fun getHeaderFieldKey(position: Int): String? { + return try { + val headers = headers + if (position < 0 || position >= headers.size) null else headers.name(position) + } catch (e: IOException) { + null + } + } + + override fun getHeaderFields(): Map> { + return try { + toMultimap(headers, statusLineToString(getResponse(true))) + } catch (e: IOException) { + emptyMap() + } + } + + override fun getRequestProperties(): Map> { + if (connected) { + throw IllegalStateException( + "Cannot access request header fields after connection is set" + ) + } + return toMultimap(requestHeaders.build(), null) + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + if (!doInput) { + throw ProtocolException("This protocol does not support input") + } + val response = getResponse(false) + if (response.code >= HTTP_BAD_REQUEST) throw FileNotFoundException(url.toString()) + return response.body.byteStream() + } + + @Throws(IOException::class) + override fun getOutputStream(): OutputStream { + val requestBody = buildCall().request().body as OutputStreamRequestBody? + ?: throw ProtocolException("method does not support a request body: $method") + if (requestBody is StreamedRequestBody) { + connect() + networkInterceptor.proceed() + } + if (requestBody.closed) { + throw ProtocolException("cannot write request body after response has been read") + } + return requestBody.outputStream!! + } + + override fun getPermission(): Permission { + val url = getURL() + var hostname = url.host + var hostPort = if (url.port != -1) url.port else HttpUrl.defaultPort(url.protocol) + if (usingProxy()) { + val proxyAddress = client.proxy!!.address() as InetSocketAddress + hostname = proxyAddress.hostName + hostPort = proxyAddress.port + } + return SocketPermission("$hostname:$hostPort", "connect, resolve") + } + + override fun getRequestProperty(field: String?): String? { + return if (field == null) null else requestHeaders[field] + } + + override fun setConnectTimeout(timeoutMillis: Int) { + client = client.newBuilder() + .connectTimeout(timeoutMillis.toLong(), TimeUnit.MILLISECONDS) + .build() + } + + override fun setInstanceFollowRedirects(followRedirects: Boolean) { + client = client.newBuilder() + .followRedirects(followRedirects) + .build() + } + + override fun getInstanceFollowRedirects(): Boolean { + return client.followRedirects + } + + override fun getConnectTimeout(): Int { + return client.connectTimeoutMillis + } + + override fun setReadTimeout(timeoutMillis: Int) { + client = client.newBuilder() + .readTimeout(timeoutMillis.toLong(), TimeUnit.MILLISECONDS) + .build() + } + + override fun getReadTimeout(): Int { + return client.readTimeoutMillis + } + + @Throws(IOException::class) + private fun buildCall(): Call { + if (call != null) { + return call as Call + } + connected = true + if (doOutput) { + if (method == "GET") { + method = "POST" + } else if (!permitsRequestBody(method)) { + throw ProtocolException("$method does not support writing") + } + } + if (requestHeaders["User-Agent"] == null) { + requestHeaders.add("User-Agent", defaultUserAgent()) + } + var requestBody: OutputStreamRequestBody? = null + if (permitsRequestBody(method)) { + var contentType: String? = requestHeaders["Content-Type"] + if (contentType == null) { + contentType = "application/x-www-form-urlencoded" + requestHeaders.add("Content-Type", contentType) + } + val stream = fixedContentLength != -1 || chunkLength > 0 + var contentLength = -1L + val contentLengthString: String? = requestHeaders["Content-Length"] + if (fixedContentLength != -1) { + contentLength = fixedContentLength.toLong() + } else if (contentLengthString != null) { + contentLength = contentLengthString.toLong() + } + requestBody = + if (stream) StreamedRequestBody(contentLength) else BufferedRequestBody( + contentLength + ) + requestBody.timeout!!.timeout( + client.writeTimeoutMillis.toLong(), + TimeUnit.MILLISECONDS + ) + } + val url: HttpUrl + try { + url = getURL().toString().toHttpUrl() + } catch (e: IllegalArgumentException) { + val malformedUrl = MalformedURLException() + malformedUrl.initCause(e) + throw malformedUrl + } + val request: Request = Request.Builder() + .url(url) + .headers(requestHeaders.build()) + .method(method, requestBody) + .build() + val clientBuilder: OkHttpClient.Builder = client.newBuilder() + clientBuilder.interceptors().clear() + clientBuilder.interceptors().add(UnexpectedException.INTERCEPTOR) + + clientBuilder.networkInterceptors().clear() + clientBuilder.networkInterceptors().add(networkInterceptor) + clientBuilder.addNetworkInterceptor { chain -> + var request1 = chain.request() + val enableCookieJar = request1.header(cookieJarHeader) != null + + if (enableCookieJar) { + val requestBuilder = request1.newBuilder() + requestBuilder.removeHeader(cookieJarHeader) + request1 = CookieManager.loadRequest(requestBuilder.build()) + } + + val networkResponse = chain.proceed(request1) + + if (enableCookieJar) { + CookieManager.saveResponse(networkResponse) + } + networkResponse + } + + // Use a separate dispatcher so that limits aren't impacted. But use the same executor service! + clientBuilder.dispatcher(Dispatcher(client.dispatcher.executorService)) + + // If we're currently not using caches, make sure the engine's client doesn't have one. + if (!getUseCaches()) { + clientBuilder.cache(null) + } + return clientBuilder.build().newCall(request).also { call = it } + } + + @Throws(IOException::class) + private fun getResponse(networkResponseOnError: Boolean): Response { + synchronized(lock) { + if (response != null) return response as Response + if (callFailure != null) { + if (networkResponseOnError && networkResponse != null) return networkResponse as Response + throw propagate(callFailure) + } + } + val call = buildCall() + networkInterceptor.proceed() + val requestBody = call.request().body as OutputStreamRequestBody? + if (requestBody != null) requestBody.outputStream!!.close() + if (executed) { + synchronized(lock) { + try { + while (response == null && callFailure == null) { + lock.wait() // Wait until the response is returned or the call fails. + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException() + } + } + } else { + executed = true + try { + onResponse(call, call.execute()) + } catch (e: IOException) { + onFailure(call, e) + } + } + synchronized(lock) { + if (callFailure != null) throw propagate(callFailure) + if (response != null) return response as Response + } + throw AssertionError() + } + + override fun usingProxy(): Boolean { + if (proxy != null) return true + val clientProxy = client.proxy + return clientProxy != null && clientProxy.type() != Proxy.Type.DIRECT + } + + @Throws(IOException::class) + override fun getResponseMessage(): String { + return getResponse(true).message + } + + @Throws(IOException::class) + override fun getResponseCode(): Int { + return getResponse(true).code + } + + override fun setRequestProperty(field: String?, newValue: String?) { + if (connected) { + throw IllegalStateException("Cannot set request property after connection is made") + } + if (field == null) { + throw NullPointerException("field == null") + } + if (newValue == null) { + return + } + requestHeaders[field] = newValue + } + + override fun setIfModifiedSince(newValue: Long) { + super.setIfModifiedSince(newValue) + if (ifModifiedSince != 0L) { + requestHeaders["If-Modified-Since"] = format(Date(ifModifiedSince)) + } else { + requestHeaders.removeAll("If-Modified-Since") + } + } + + override fun addRequestProperty(field: String?, value: String?) { + if (connected) { + throw IllegalStateException("Cannot add request property after connection is made") + } + if (field == null) { + throw NullPointerException("field == null") + } + if (value == null) { + return + } + requestHeaders.add(field, value) + } + + @Throws(ProtocolException::class) + override fun setRequestMethod(method: String) { + if (!METHODS.contains(method)) { + throw ProtocolException("Expected one of $METHODS but was $method") + } + this.method = method + } + + override fun setFixedLengthStreamingMode(contentLength: Int) { + setFixedLengthStreamingMode(contentLength.toLong()) + } + + override fun setFixedLengthStreamingMode(contentLength: Long) { + if (super.connected) throw IllegalStateException("Already connected") + if (chunkLength > 0) throw IllegalStateException("Already in chunked mode") + if (contentLength < 0) throw IllegalArgumentException("contentLength < 0") + this.fixedContentLength = contentLength.toInt() + super.fixedContentLength = contentLength.toInt().coerceAtMost(Int.MAX_VALUE) + } + + override fun onFailure(call: Call, e: IOException) { + synchronized(lock) { + callFailure = if ((e is UnexpectedException)) e.cause else e + lock.notifyAll() + } + } + + override fun onResponse(call: Call, response: Response) { + synchronized(lock) { + this.response = response + handshake = response.handshake + url = response.request.url.toUrl() + lock.notifyAll() + } + } + + internal inner class NetworkInterceptor : Interceptor { + // Guarded by HttpUrlConnection.this. + private var proceed = false + fun proceed() { + synchronized(lock) { + proceed = true + lock.notifyAll() + } + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + var request: Request = chain.request() + synchronized(lock) { + connectPending = false + proxy = chain.connection()!!.route().proxy + handshake = chain.connection()!!.handshake() + lock.notifyAll() + try { + while (!proceed) { + lock.wait() // Wait until proceed() is called. + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException() + } + } + + // Try to lock in the Content-Length before transmitting the request body. + if (request.body is OutputStreamRequestBody) { + val requestBody = request.body as OutputStreamRequestBody? + request = requestBody!!.prepareToSendRequest(request) + } + val response: Response = chain.proceed(request) + synchronized(lock) { + networkResponse = response + url = response.request.url.toUrl() + } + return response + } + } + } + + internal abstract class OutputStreamRequestBody : RequestBody() { + var timeout: Timeout? = null + var expectedContentLength: Long = 0 + var outputStream: OutputStream? = null + var closed = false + fun initOutputStream(sink: BufferedSink, expectedContentLength: Long) { + timeout = sink.timeout() + this.expectedContentLength = expectedContentLength + + // An output stream that writes to sink. If expectedContentLength is not -1, then this expects + // exactly that many bytes to be written. + outputStream = object : OutputStream() { + private var bytesReceived: Long = 0 + + @Throws(IOException::class) + override fun write(b: Int) { + write(byteArrayOf(b.toByte()), 0, 1) + } + + @Throws(IOException::class) + override fun write(source: ByteArray, offset: Int, byteCount: Int) { + if (closed) throw IOException("closed") // Not IllegalStateException! + if (expectedContentLength != -1L && bytesReceived + byteCount > expectedContentLength) { + throw ProtocolException( + "expected " + expectedContentLength + + " bytes but received " + bytesReceived + byteCount + ) + } + bytesReceived += byteCount.toLong() + try { + sink.write(source, offset, byteCount) + } catch (e: InterruptedIOException) { + throw SocketTimeoutException(e.message) + } + } + + @Throws(IOException::class) + override fun flush() { + if (closed) return // Weird, but consistent with historical behavior. + sink.flush() + } + + @Throws(IOException::class) + override fun close() { + closed = true + if (expectedContentLength != -1L && bytesReceived < expectedContentLength) { + throw ProtocolException( + ("expected " + expectedContentLength + + " bytes but received " + bytesReceived) + ) + } + sink.close() + } + } + } + + override fun contentLength(): Long { + return expectedContentLength + } + + override fun contentType(): MediaType? { + return null // Let the caller provide this in a regular header. + } + + @Throws(IOException::class) + open fun prepareToSendRequest(request: Request): Request { + return request + } + } + + @Suppress("MemberVisibilityCanBePrivate") + internal class BufferedRequestBody(expectedContentLength: Long) : + OutputStreamRequestBody() { + val buffer = Buffer() + var contentLength = -1L + + init { + initOutputStream(buffer, expectedContentLength) + } + + override fun contentLength(): Long { + return contentLength + } + + @Throws(IOException::class) + override fun prepareToSendRequest(request: Request): Request { + if (request.header("Content-Length") != null) return request + outputStream!!.close() + contentLength = buffer.size + return request.newBuilder() + .removeHeader("Transfer-Encoding") + .header("Content-Length", buffer.size.toString()) + .build() + } + + override fun writeTo(sink: BufferedSink) { + buffer.copyTo(sink.buffer, 0, buffer.size) + } + } + + internal class StreamedRequestBody(expectedContentLength: Long) : + OutputStreamRequestBody() { + private val pipe = Pipe(8192) + + init { + initOutputStream(pipe.sink.buffer(), expectedContentLength) + } + + override fun isOneShot(): Boolean { + return true + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + val buffer = Buffer() + while (pipe.source.read(buffer, 8192) != -1L) { + sink.write(buffer, buffer.size) + } + } + } + + internal abstract class DelegatingHttpsURLConnection(private val delegate: HttpURLConnection) : + HttpsURLConnection(delegate.url) { + protected abstract fun handshake(): Handshake? + abstract override fun setHostnameVerifier(hostnameVerifier: HostnameVerifier) + abstract override fun getHostnameVerifier(): HostnameVerifier + abstract override fun setSSLSocketFactory(sslSocketFactory: SSLSocketFactory?) + abstract override fun getSSLSocketFactory(): SSLSocketFactory + override fun getCipherSuite(): String? { + val handshake = handshake() + return handshake?.cipherSuite?.javaName + } + + override fun getLocalCertificates(): Array? { + val handshake = handshake() ?: return null + val result = handshake.localCertificates + return if (result.isNotEmpty()) result.toTypedArray() else null + } + + override fun getServerCertificates(): Array? { + val handshake = handshake() ?: return null + val result = handshake.peerCertificates + return if (result.isNotEmpty()) result.toTypedArray() else null + } + + override fun getPeerPrincipal(): Principal? { + return handshake()?.peerPrincipal + } + + override fun getLocalPrincipal(): Principal? { + return handshake()?.localPrincipal + } + + @Throws(IOException::class) + override fun connect() { + connected = true + delegate.connect() + } + + override fun disconnect() { + delegate.disconnect() + } + + override fun getErrorStream(): InputStream? { + return delegate.errorStream + } + + override fun getRequestMethod(): String { + return delegate.requestMethod + } + + @Throws(IOException::class) + override fun getResponseCode(): Int { + return delegate.responseCode + } + + @Throws(IOException::class) + override fun getResponseMessage(): String? { + return delegate.responseMessage + } + + @Throws(ProtocolException::class) + override fun setRequestMethod(method: String) { + delegate.requestMethod = method + } + + override fun usingProxy(): Boolean { + return delegate.usingProxy() + } + + override fun getInstanceFollowRedirects(): Boolean { + return delegate.instanceFollowRedirects + } + + override fun setInstanceFollowRedirects(followRedirects: Boolean) { + delegate.instanceFollowRedirects = followRedirects + } + + override fun getAllowUserInteraction(): Boolean { + return delegate.allowUserInteraction + } + + @Throws(IOException::class) + override fun getContent(): Any { + return delegate.content + } + + @Throws(IOException::class) + override fun getContent(types: Array?>?): Any? { + return delegate.getContent(types) + } + + override fun getContentEncoding(): String? { + return delegate.contentEncoding + } + + override fun getContentLength(): Int { + return delegate.contentLength + } + + // Should only be invoked on Java 8+ or Android API 24+. + @RequiresApi(Build.VERSION_CODES.N) + override fun getContentLengthLong(): Long { + return delegate.contentLengthLong + } + + override fun getContentType(): String? { + return delegate.contentType + } + + override fun getDate(): Long { + return delegate.date + } + + override fun getDefaultUseCaches(): Boolean { + return delegate.defaultUseCaches + } + + override fun getDoInput(): Boolean { + return delegate.doInput + } + + override fun getDoOutput(): Boolean { + return delegate.doOutput + } + + override fun getExpiration(): Long { + return delegate.expiration + } + + override fun getHeaderField(pos: Int): String? { + return delegate.getHeaderField(pos) + } + + override fun getHeaderFields(): Map> { + return delegate.headerFields + } + + override fun getRequestProperties(): Map> { + return delegate.requestProperties + } + + override fun addRequestProperty(field: String, newValue: String) { + delegate.addRequestProperty(field, newValue) + } + + override fun getHeaderField(key: String): String? { + return delegate.getHeaderField(key) + } + + // Should only be invoked on Java 8+ or Android API 24+. + @RequiresApi(Build.VERSION_CODES.N) + override fun getHeaderFieldLong(field: String, defaultValue: Long): Long { + return delegate.getHeaderFieldLong(field, defaultValue) + } + + override fun getHeaderFieldDate(field: String, defaultValue: Long): Long { + return delegate.getHeaderFieldDate(field, defaultValue) + } + + override fun getHeaderFieldInt(field: String, defaultValue: Int): Int { + return delegate.getHeaderFieldInt(field, defaultValue) + } + + override fun getHeaderFieldKey(position: Int): String? { + return delegate.getHeaderFieldKey(position) + } + + override fun getIfModifiedSince(): Long { + return delegate.ifModifiedSince + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + return delegate.inputStream + } + + override fun getLastModified(): Long { + return delegate.lastModified + } + + @Throws(IOException::class) + override fun getOutputStream(): OutputStream { + return delegate.outputStream + } + + @Throws(IOException::class) + override fun getPermission(): Permission { + return delegate.permission + } + + override fun getRequestProperty(field: String): String? { + return delegate.getRequestProperty(field) + } + + override fun getURL(): URL { + return delegate.url + } + + override fun getUseCaches(): Boolean { + return delegate.useCaches + } + + override fun setAllowUserInteraction(newValue: Boolean) { + delegate.allowUserInteraction = newValue + } + + override fun setDefaultUseCaches(newValue: Boolean) { + delegate.defaultUseCaches = newValue + } + + override fun setDoInput(newValue: Boolean) { + delegate.doInput = newValue + } + + override fun setDoOutput(newValue: Boolean) { + delegate.doOutput = newValue + } + + // Should only be invoked on Java 8+ or Android API 24+. + override fun setFixedLengthStreamingMode(contentLength: Long) { + delegate.setFixedLengthStreamingMode(contentLength) + } + + override fun setIfModifiedSince(newValue: Long) { + delegate.ifModifiedSince = newValue + } + + override fun setRequestProperty(field: String, newValue: String) { + delegate.setRequestProperty(field, newValue) + } + + override fun setUseCaches(newValue: Boolean) { + delegate.useCaches = newValue + } + + override fun setConnectTimeout(timeoutMillis: Int) { + delegate.connectTimeout = timeoutMillis + } + + override fun getConnectTimeout(): Int { + return delegate.connectTimeout + } + + override fun setReadTimeout(timeoutMillis: Int) { + delegate.readTimeout = timeoutMillis + } + + override fun getReadTimeout(): Int { + return delegate.readTimeout + } + + override fun toString(): String { + return delegate.toString() + } + + override fun setFixedLengthStreamingMode(contentLength: Int) { + delegate.setFixedLengthStreamingMode(contentLength) + } + + override fun setChunkedStreamingMode(chunkLength: Int) { + delegate.setChunkedStreamingMode(chunkLength) + } + } + + internal class OkHttpsURLConnection(private val delegate: OkHttpURLConnection) : + DelegatingHttpsURLConnection(delegate) { + constructor(url: URL?, client: OkHttpClient) : this(OkHttpURLConnection(url, client)) + + override fun handshake(): Handshake? { + if (delegate.call == null) { + throw IllegalStateException("Connection has not yet been established") + } + return delegate.handshake + } + + override fun setHostnameVerifier(hostnameVerifier: HostnameVerifier) { + delegate.client = delegate.client.newBuilder() + .hostnameVerifier(hostnameVerifier) + .build() + } + + override fun getHostnameVerifier(): HostnameVerifier { + return delegate.client.hostnameVerifier + } + + override fun setSSLSocketFactory(sslSocketFactory: SSLSocketFactory?) { + if (sslSocketFactory == null) { + throw IllegalArgumentException("sslSocketFactory == null") + } + // This fails in JDK 9 because OkHttp is unable to extract the trust manager. + delegate.client = delegate.client.newBuilder() + .sslSocketFactory(sslSocketFactory, unsafeTrustManager) + .build() + } + + override fun getSSLSocketFactory(): SSLSocketFactory { + return delegate.client.sslSocketFactory + } + } + + internal class UnexpectedException(cause: Throwable?) : IOException(cause) { + companion object { + val INTERCEPTOR = Interceptor { chain: Interceptor.Chain -> + try { + return@Interceptor chain.proceed(chain.request()) + } catch (e: Error) { + throw UnexpectedException(e) + } catch (e: RuntimeException) { + throw UnexpectedException(e) + } + } + } + } + + companion object { + const val SELECTED_PROTOCOL = "ObsoleteUrlFactory-Selected-Protocol" + const val RESPONSE_SOURCE = "ObsoleteUrlFactory-Response-Source" + val METHODS: Set = LinkedHashSet( + listOf("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH") + ) + val UTC: TimeZone = TimeZone.getTimeZone("GMT") + const val HTTP_CONTINUE = 100 + val STANDARD_DATE_FORMAT: ThreadLocal = ThreadLocal.withInitial { + + // Date format specified by RFC 7231 section 7.1.1.1. + val rfc1123: DateFormat = SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss 'GMT'", + Locale.US + ) + rfc1123.isLenient = false + rfc1123.timeZone = UTC + rfc1123 + } + private val FIELD_NAME_COMPARATOR = + java.util.Comparator { a: String?, b: String? -> + // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + if (a === b) { + return@Comparator 0 + } else if (a == null) { + return@Comparator -1 + } else if (b == null) { + return@Comparator 1 + } else { + return@Comparator java.lang.String.CASE_INSENSITIVE_ORDER.compare(a, b) + } + } + + @Suppress( + "RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", + "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS" + ) + fun format(value: Date?): String { + return STANDARD_DATE_FORMAT.get().format(value) + } + + fun permitsRequestBody(method: String): Boolean { + return !((method == "GET") || (method == "HEAD")) + } + + /** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */ + fun hasBody(response: Response): Boolean { + // HEAD requests never yield a body regardless of the response headers. + if ((response.request.method == "HEAD")) { + return false + } + val responseCode = response.code + if (((responseCode < HTTP_CONTINUE || responseCode >= 200) + && (responseCode != HttpURLConnection.HTTP_NO_CONTENT + ) && (responseCode != HttpURLConnection.HTTP_NOT_MODIFIED)) + ) { + return true + } + + // If the Content-Length or Transfer-Encoding headers disagree with the response code, the + // response is malformed. For best compatibility, we honor the headers. + return (contentLength(response.headers) != -1L + || "chunked".equals( + response.header("Transfer-Encoding"), + ignoreCase = true + )) + } + + fun contentLength(headers: Headers): Long { + val s = headers["Content-Length"] ?: return -1 + return try { + s.toLong() + } catch (e: NumberFormatException) { + -1 + } + } + + fun responseSourceHeader(response: Response): String { + if (response.networkResponse == null) { + return if (response.cacheResponse == null) "NONE" else "CACHE " + response.code + } + return if (response.cacheResponse == null) "NETWORK " + response.code else "CONDITIONAL_CACHE " + response.networkResponse!!.code + } + + fun statusLineToString(response: Response): String { + return ((if (response.protocol == Protocol.HTTP_1_0) "HTTP/1.0" else "HTTP/1.1") + + ' ' + response.code + + ' ' + response.message) + } + + fun toHumanReadableAscii(s: String): String { + var i = 0 + val length = s.length + var c: Int + while (i < length) { + c = s.codePointAt(i) + if (c > '\u001f'.code && c < '\u007f'.code) { + i += Character.charCount(c) + continue + } + val buffer = Buffer() + buffer.writeUtf8(s, 0, i) + buffer.writeUtf8CodePoint('?'.code) + var j = i + Character.charCount(c) + while (j < length) { + c = s.codePointAt(j) + buffer.writeUtf8CodePoint( + (if (c > '\u001f'.code && c < '\u007f'.code) c else '?') as Int + ) + j += Character.charCount(c) + } + return buffer.readUtf8() + } + return s + } + + fun toMultimap(headers: Headers, valueForNullKey: String?): Map> { + val result: MutableMap> = TreeMap(FIELD_NAME_COMPARATOR) + var i = 0 + val size = headers.size + while (i < size) { + val fieldName = headers.name(i) + val value = headers.value(i) + val allValues: MutableList = ArrayList() + val otherValues = result[fieldName] + if (otherValues != null) { + allValues.addAll(otherValues) + } + allValues.add(value) + result[fieldName] = Collections.unmodifiableList(allValues) + i++ + } + if (valueForNullKey != null) { + result[null] = Collections.unmodifiableList(listOf(valueForNullKey)) + } + return Collections.unmodifiableMap(result) + } + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + fun getSystemProperty(key: String?, defaultValue: String?): String? { + val value: String? + try { + value = System.getProperty(key) + } catch (ex: AccessControlException) { + return defaultValue + } + return value ?: defaultValue + } + + fun defaultUserAgent(): String { + val agent = getSystemProperty("http.agent", null) + return if (agent != null) toHumanReadableAscii(agent) else "ObsoleteUrlFactory" + } + + @Throws(IOException::class) + fun propagate(throwable: Throwable?): IOException { + if (throwable is IOException) throw throwable + if (throwable is Error) throw throwable + if (throwable is RuntimeException) throw throwable + throw AssertionError() + } + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE") + private inline fun Any.wait() = (this as Object).wait() + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE") + private inline fun Any.notify() = (this as Object).notify() + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE") + private inline fun Any.notifyAll() = (this as Object).notifyAll() + + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + val okHttpClient = OkHttpClient() + URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient)) + val url = URL("https://publicobject.com/helloworld.txt") + val urlConnection = url.openConnection() as HttpURLConnection + BufferedReader( + InputStreamReader(urlConnection.inputStream) + ).use { reader -> + var line: String? + while ((reader.readLine().also { line = it }) != null) { + println(line) + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/help/http/OkHttpExceptionInterceptor.kt b/app/src/main/java/io/legado/app/help/http/OkHttpExceptionInterceptor.kt new file mode 100755 index 000000000..018044274 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/OkHttpExceptionInterceptor.kt @@ -0,0 +1,20 @@ +package io.legado.app.help.http + +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +object OkHttpExceptionInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + try { + return chain.proceed(chain.request()) + } catch (e: IOException) { + throw e + } catch (e: Throwable) { + throw IOException(e) + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt new file mode 100755 index 000000000..d16092d0a --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt @@ -0,0 +1,198 @@ +package io.legado.app.help.http + +import io.legado.app.utils.EncodingDetect +import io.legado.app.utils.GSON +import io.legado.app.utils.Utf8BomUtils +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.internal.http.RealResponseBody +import okio.buffer +import okio.source +import java.io.File +import java.io.IOException +import java.nio.charset.Charset +import java.util.zip.ZipInputStream +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +suspend fun OkHttpClient.newCallResponse( + retry: Int = 0, + builder: Request.Builder.() -> Unit +): Response { + val requestBuilder = Request.Builder() + requestBuilder.apply(builder) + var response: Response? = null + for (i in 0..retry) { + response = newCall(requestBuilder.build()).await() + if (response.isSuccessful) { + return response + } + } + return response!! +} + +suspend fun OkHttpClient.newCallResponseBody( + retry: Int = 0, + builder: Request.Builder.() -> Unit +): ResponseBody { + return newCallResponse(retry, builder).body +} + +suspend fun OkHttpClient.newCallStrResponse( + retry: Int = 0, + builder: Request.Builder.() -> Unit +): StrResponse { + return newCallResponse(retry, builder).let { + StrResponse(it, it.body.text()) + } +} + +suspend fun Call.await(): Response = suspendCancellableCoroutine { block -> + + block.invokeOnCancellation { + cancel() + } + + enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + block.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + block.resume(response) + } + }) + +} + +fun ResponseBody.text(encode: String? = null): String { + val responseBytes = Utf8BomUtils.removeUTF8BOM(bytes()) + var charsetName: String? = encode + + charsetName?.let { + return String(responseBytes, Charset.forName(charsetName)) + } + + //根据http头判断 + contentType()?.charset()?.let { charset -> + return String(responseBytes, charset) + } + + //根据内容判断 + charsetName = EncodingDetect.getHtmlEncode(responseBytes) + return String(responseBytes, Charset.forName(charsetName)) +} + +fun ResponseBody.decompressed(): ResponseBody { + val contentType = contentType()?.toString() + if (contentType != "application/zip") { + return this + } + val source = ZipInputStream(byteStream()).apply { + try { + nextEntry + } catch (e: Exception) { + close() + throw e + } + }.source().buffer() + return RealResponseBody(null, -1, source) +} + +fun Request.Builder.addHeaders(headers: Map) { + headers.forEach { + addHeader(it.key, it.value) + } +} + +fun Request.Builder.get(url: String, queryMap: Map, encoded: Boolean = false) { + val httpBuilder = url.toHttpUrl().newBuilder() + queryMap.forEach { + if (encoded) { + httpBuilder.addEncodedQueryParameter(it.key, it.value) + } else { + httpBuilder.addQueryParameter(it.key, it.value) + } + } + url(httpBuilder.build()) +} + +fun Request.Builder.get(url: String, encodedQuery: String?) { + val httpBuilder = url.toHttpUrl().newBuilder() + httpBuilder.encodedQuery(encodedQuery) + url(httpBuilder.build()) +} + +private val formContentType = "application/x-www-form-urlencoded".toMediaType() + +fun Request.Builder.postForm(encodedForm: String) { + post(encodedForm.toRequestBody(formContentType)) +} + +@Suppress("unused") +fun Request.Builder.postForm(form: Map, encoded: Boolean = false) { + val formBody = FormBody.Builder() + form.forEach { + if (encoded) { + formBody.addEncoded(it.key, it.value) + } else { + formBody.add(it.key, it.value) + } + } + post(formBody.build()) +} + +fun Request.Builder.postMultipart(type: String?, form: Map) { + val multipartBody = MultipartBody.Builder() + type?.let { + multipartBody.setType(type.toMediaType()) + } + form.forEach { + when (val value = it.value) { + is Map<*, *> -> { + val fileName = value["fileName"] as String + val file = value["file"] + val mediaType = (value["contentType"] as? String)?.toMediaType() + val requestBody = when (file) { + is File -> { + file.asRequestBody(mediaType) + } + + is ByteArray -> { + file.toRequestBody(mediaType) + } + + is String -> { + file.toRequestBody(mediaType) + } + + else -> { + GSON.toJson(file).toRequestBody(mediaType) + } + } + multipartBody.addFormDataPart(it.key, fileName, requestBody) + } + + else -> multipartBody.addFormDataPart(it.key, it.value.toString()) + } + } + post(multipartBody.build()) +} + +fun Request.Builder.postJson(json: String?) { + json?.let { + val requestBody = json.toRequestBody("application/json; charset=UTF-8".toMediaType()) + post(requestBody) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/OkhttpUncaughtExceptionHandler.kt b/app/src/main/java/io/legado/app/help/http/OkhttpUncaughtExceptionHandler.kt new file mode 100755 index 000000000..69b526ea9 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/OkhttpUncaughtExceptionHandler.kt @@ -0,0 +1,11 @@ +package io.legado.app.help.http + +import io.legado.app.constant.AppLog + +object OkhttpUncaughtExceptionHandler : Thread.UncaughtExceptionHandler { + + override fun uncaughtException(t: Thread, e: Throwable) { + AppLog.put("Okhttp Dispatcher中的线程执行出错\n${e.localizedMessage}", e) + } + +} diff --git a/app/src/main/java/io/legado/app/help/http/RequestMethod.kt b/app/src/main/java/io/legado/app/help/http/RequestMethod.kt new file mode 100755 index 000000000..bba9f9761 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/RequestMethod.kt @@ -0,0 +1,5 @@ +package io.legado.app.help.http + +enum class RequestMethod { + GET, POST +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt new file mode 100755 index 000000000..e540282c7 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt @@ -0,0 +1,194 @@ +package io.legado.app.help.http + +import android.annotation.SuppressLint +import android.net.http.X509TrustManagerExtensions +import io.legado.app.utils.printOnDebug + + +import java.io.IOException +import java.io.InputStream +import java.security.KeyManagementException +import java.security.KeyStore +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import javax.net.ssl.* + +@Suppress("unused") +object SSLHelper { + + /** + * 为了解决客户端不信任服务器数字证书的问题, + * 网络上大部分的解决方案都是让客户端不对证书做任何检查, + * 这是一种有很大安全漏洞的办法 + */ + val unsafeTrustManager: X509TrustManager = + @SuppressLint("CustomX509TrustManager") + object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + //do nothing,接受任意客户端证书 + } + + @SuppressLint("TrustAllX509TrustManager") + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + //do nothing,接受任意客户端证书 + } + + fun checkServerTrusted(chain: Array, authType: String, host: String): List { + return chain.toList() + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + } + + val unsafeTrustManagerExtensions by lazy { + X509TrustManagerExtensions(unsafeTrustManager) + } + + val unsafeSSLSocketFactory: SSLSocketFactory by lazy { + try { + val sslContext = SSLContext.getInstance("SSL") + sslContext.init(null, arrayOf(unsafeTrustManager), SecureRandom()) + sslContext.socketFactory + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + /** + * 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配, + * 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。 + * 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true + */ + val unsafeHostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true } + + class SSLParams { + lateinit var sSLSocketFactory: SSLSocketFactory + lateinit var trustManager: X509TrustManager + } + + /** + * https单向认证 + * 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证 + */ + fun getSslSocketFactory(trustManager: X509TrustManager): SSLParams? { + return getSslSocketFactoryBase(trustManager, null, null) + } + + /** + * https单向认证 + * 用含有服务端公钥的证书校验服务端证书 + */ + fun getSslSocketFactory(vararg certificates: InputStream): SSLParams? { + return getSslSocketFactoryBase(null, null, null, *certificates) + } + + /** + * https双向认证 + * bksFile 和 password -> 客户端使用bks证书校验服务端证书 + * certificates -> 用含有服务端公钥的证书校验服务端证书 + */ + fun getSslSocketFactory( + bksFile: InputStream, + password: String, + vararg certificates: InputStream + ): SSLParams? { + return getSslSocketFactoryBase(null, bksFile, password, *certificates) + } + + /** + * https双向认证 + * bksFile 和 password -> 客户端使用bks证书校验服务端证书 + * X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可 + */ + fun getSslSocketFactory( + bksFile: InputStream, + password: String, + trustManager: X509TrustManager + ): SSLParams? { + return getSslSocketFactoryBase(trustManager, bksFile, password) + } + + private fun getSslSocketFactoryBase( + trustManager: X509TrustManager?, + bksFile: InputStream?, + password: String?, + vararg certificates: InputStream + ): SSLParams? { + val sslParams = SSLParams() + try { + val keyManagers = prepareKeyManager(bksFile, password) + val trustManagers = prepareTrustManager(*certificates) + val manager: X509TrustManager = trustManager ?: chooseTrustManager(trustManagers) + // 创建TLS类型的SSLContext对象, that uses our TrustManager + val sslContext = SSLContext.getInstance("TLS") + // 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书 + // 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书 + sslContext.init(keyManagers, arrayOf(manager), null) + // 通过sslContext获取SSLSocketFactory对象 + sslParams.sSLSocketFactory = sslContext.socketFactory + sslParams.trustManager = manager + return sslParams + } catch (e: NoSuchAlgorithmException) { + e.printOnDebug() + } catch (e: KeyManagementException) { + e.printOnDebug() + } + return null + } + + private fun prepareKeyManager(bksFile: InputStream?, password: String?): Array? { + try { + if (bksFile == null || password == null) return null + val clientKeyStore = KeyStore.getInstance("BKS") + clientKeyStore.load(bksFile, password.toCharArray()) + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(clientKeyStore, password.toCharArray()) + return kmf.keyManagers + } catch (e: Exception) { + e.printOnDebug() + } + return null + } + + private fun prepareTrustManager(vararg certificates: InputStream): Array { + val certificateFactory = CertificateFactory.getInstance("X.509") + // 创建一个默认类型的KeyStore,存储我们信任的证书 + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null) + for ((index, certStream) in certificates.withIndex()) { + val certificateAlias = index.toString() + // 证书工厂根据证书文件的流生成证书 cert + val cert = certificateFactory.generateCertificate(certStream) + // 将 cert 作为可信证书放入到keyStore中 + keyStore.setCertificateEntry(certificateAlias, cert) + try { + certStream.close() + } catch (e: IOException) { + e.printOnDebug() + } + } + //我们创建一个默认类型的TrustManagerFactory + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + //用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书 + tmf.init(keyStore) + //通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书 + return tmf.trustManagers + } + + private fun chooseTrustManager(trustManagers: Array): X509TrustManager { + for (trustManager in trustManagers) { + if (trustManager is X509TrustManager) { + return trustManager + } + } + throw NullPointerException() + } +} diff --git a/app/src/main/java/io/legado/app/help/http/StrResponse.kt b/app/src/main/java/io/legado/app/help/http/StrResponse.kt new file mode 100755 index 000000000..46fed6291 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/StrResponse.kt @@ -0,0 +1,84 @@ +package io.legado.app.help.http + +import androidx.annotation.Keep +import okhttp3.Headers +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.Response.Builder +import okhttp3.ResponseBody + +/** + * An HTTP response. + */ +@Keep +@Suppress("unused", "MemberVisibilityCanBePrivate") +class StrResponse { + var raw: Response + private set + var body: String? = null + private set + var errorBody: ResponseBody? = null + private set + + constructor(rawResponse: Response, body: String?) { + this.raw = rawResponse + this.body = body + } + + constructor(url: String, body: String?) { + val request = try { + Request.Builder().url(url).build() + } catch (e: Exception) { + Request.Builder().url("http://localhost/").build() + } + raw = Builder() + .code(200) + .message("OK") + .protocol(Protocol.HTTP_1_1) + .request(request) + .build() + this.body = body + } + + constructor(rawResponse: Response, errorBody: ResponseBody?) { + this.raw = rawResponse + this.errorBody = errorBody + } + + fun raw() = raw + + fun url(): String { + raw.networkResponse?.let { + return it.request.url.toString() + } + return raw.request.url.toString() + } + + val url: String get() = url() + + fun body() = body + + fun code(): Int { + return raw.code + } + + fun message(): String { + return raw.message + } + + fun headers(): Headers { + return raw.headers + } + + fun isSuccessful(): Boolean = raw.isSuccessful + + fun errorBody(): ResponseBody? { + return errorBody + } + + override fun toString(): String { + return raw.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/api/CookieManagerInterface.kt b/app/src/main/java/io/legado/app/help/http/api/CookieManagerInterface.kt new file mode 100755 index 000000000..927ba4faf --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/api/CookieManagerInterface.kt @@ -0,0 +1,28 @@ +package io.legado.app.help.http.api + +interface CookieManagerInterface { + + /** + * 保存cookie + */ + fun setCookie(url: String, cookie: String?) + + /** + * 替换cookie + */ + fun replaceCookie(url: String, cookie: String) + + /** + * 获取cookie + */ + fun getCookie(url: String): String + + /** + * 移除cookie + */ + fun removeCookie(url: String) + + fun cookieToMap(cookie: String): MutableMap + + fun mapToCookie(cookieMap: Map?): String? +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/rhino/NativeBaseSource.kt b/app/src/main/java/io/legado/app/help/rhino/NativeBaseSource.kt new file mode 100755 index 000000000..ed50b4d88 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/rhino/NativeBaseSource.kt @@ -0,0 +1,46 @@ +package io.legado.app.help.rhino + +import com.script.rhino.JavaObjectWrapFactory +import org.mozilla.javascript.NativeJavaObject +import org.mozilla.javascript.Scriptable + +class NativeBaseSource(scope: Scriptable?, javaObject: Any, staticType: Class<*>?) : + NativeJavaObject(scope, javaObject, staticType) { + + override fun has(name: String, start: Scriptable): Boolean { + if (name != "setVariable" && name.length > 3 && name.startsWith("set")) { + val name = name.substring(3).replaceFirstChar { it.lowercase() } + if (super.has(name, start)) { + return false + } + } + return super.has(name, start) + } + + override fun get(name: String, start: Scriptable): Any? { + if (name != "setVariable" && name.length > 3 && name.startsWith("set")) { + val name = name.substring(3).replaceFirstChar { it.lowercase() } + if (super.has(name, start)) { + return NOT_FOUND + } + } + return super.get(name, start) + } + + override fun put( + name: String, + start: Scriptable, + value: Any? + ) { + if (name == "variable") { + super.put(name, start, value) + } + } + + companion object { + val factory = JavaObjectWrapFactory { scope, javaObject, staticType -> + NativeBaseSource(scope, javaObject, staticType) + } + } + +} diff --git a/app/src/main/java/io/legado/app/help/source/BaseSourceExtensions.kt b/app/src/main/java/io/legado/app/help/source/BaseSourceExtensions.kt new file mode 100755 index 000000000..5c64a1b66 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/source/BaseSourceExtensions.kt @@ -0,0 +1,21 @@ +package io.legado.app.help.source + +import io.legado.app.constant.SourceType +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.RssSource +import io.legado.app.model.SharedJsScope +import org.mozilla.javascript.Scriptable +import kotlin.coroutines.CoroutineContext + +fun BaseSource.getShareScope(coroutineContext: CoroutineContext? = null): Scriptable? { + return SharedJsScope.getScope(jsLib, coroutineContext) +} + +fun BaseSource.getSourceType(): Int { + return when (this) { + is BookSource -> SourceType.book + is RssSource -> SourceType.rss + else -> error("unknown source type: ${this::class.simpleName}.") + } +} diff --git a/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt b/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt new file mode 100755 index 000000000..6d92062b2 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt @@ -0,0 +1,121 @@ +package io.legado.app.help.source + +import com.script.rhino.runScriptWithContext +import io.legado.app.constant.BookSourceType +import io.legado.app.constant.BookType +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.rule.ExploreKind +import io.legado.app.utils.ACache +import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.printOnDebug +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap + +/** + * 采用md5作为key可以在分类修改后自动重新计算,不需要手动刷新 + */ + +private val mutexMap by lazy { hashMapOf() } +private val exploreKindsMap by lazy { ConcurrentHashMap>() } +private val aCache by lazy { ACache.get("explore") } + +private fun BookSource.getExploreKindsKey(): String { + return MD5Utils.md5Encode(bookSourceUrl + exploreUrl) +} + +private fun BookSourcePart.getExploreKindsKey(): String { + return getBookSource()!!.getExploreKindsKey() +} + +suspend fun BookSourcePart.exploreKinds(): List { + return getBookSource()!!.exploreKinds() +} + +suspend fun BookSource.exploreKinds(): List { + val exploreKindsKey = getExploreKindsKey() + exploreKindsMap[exploreKindsKey]?.let { return it } + val exploreUrl = exploreUrl + if (exploreUrl.isNullOrBlank()) { + return emptyList() + } + val mutex = mutexMap[bookSourceUrl] ?: Mutex().apply { mutexMap[bookSourceUrl] = this } + mutex.withLock { + exploreKindsMap[exploreKindsKey]?.let { return it } + val kinds = arrayListOf() + withContext(Dispatchers.IO) { + kotlin.runCatching { + var ruleStr = exploreUrl + if (exploreUrl.startsWith("", true) + || exploreUrl.startsWith("@js:", true) + ) { + ruleStr = aCache.getAsString(exploreKindsKey) + if (ruleStr.isNullOrBlank()) { + val jsStr = if (exploreUrl.startsWith("@")) { + exploreUrl.substring(4) + } else { + exploreUrl.substring(4, exploreUrl.lastIndexOf("<")) + } + ruleStr = runScriptWithContext { + evalJS(jsStr).toString().trim() + } + aCache.put(exploreKindsKey, ruleStr) + } + } + if (ruleStr.isJsonArray()) { + GSON.fromJsonArray(ruleStr).getOrThrow().let { + kinds.addAll(it) + } + } else { + ruleStr.split("(&&|\n)+".toRegex()).forEach { kindStr -> + val kindCfg = kindStr.split("::") + kinds.add(ExploreKind(kindCfg.first(), kindCfg.getOrNull(1))) + } + } + }.onFailure { + kinds.add(ExploreKind("ERROR:${it.localizedMessage}", it.stackTraceToString())) + it.printOnDebug() + } + } + exploreKindsMap[exploreKindsKey] = kinds + return kinds + } +} + +suspend fun BookSourcePart.clearExploreKindsCache() { + withContext(Dispatchers.IO) { + val exploreKindsKey = getExploreKindsKey() + aCache.remove(exploreKindsKey) + exploreKindsMap.remove(exploreKindsKey) + } +} + +suspend fun BookSource.clearExploreKindsCache() { + withContext(Dispatchers.IO) { + val exploreKindsKey = getExploreKindsKey() + aCache.remove(exploreKindsKey) + exploreKindsMap.remove(exploreKindsKey) + } +} + +fun BookSource.exploreKindsJson(): String { + val exploreKindsKey = getExploreKindsKey() + return aCache.getAsString(exploreKindsKey)?.takeIf { it.isJsonArray() } + ?: exploreUrl.takeIf { it.isJsonArray() } + ?: "" +} + +fun BookSource.getBookType(): Int { + return when (bookSourceType) { + BookSourceType.file -> BookType.text or BookType.webFile + BookSourceType.image -> BookType.image + BookSourceType.audio -> BookType.audio + else -> BookType.text + } +} diff --git a/app/src/main/java/io/legado/app/help/source/RssSourceExtensions.kt b/app/src/main/java/io/legado/app/help/source/RssSourceExtensions.kt new file mode 100755 index 000000000..230b6f72a --- /dev/null +++ b/app/src/main/java/io/legado/app/help/source/RssSourceExtensions.kt @@ -0,0 +1,62 @@ +package io.legado.app.help.source + +import io.legado.app.data.entities.RssSource +import io.legado.app.utils.ACache +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.NetworkUtils +import com.script.rhino.runScriptWithContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private val aCache by lazy { ACache.get("rssSortUrl") } + +private fun RssSource.getSortUrlsKey(): String { + return MD5Utils.md5Encode(sourceUrl + sortUrl) +} + +suspend fun RssSource.sortUrls(): List> { + return arrayListOf>().apply { + val sortUrlsKey = getSortUrlsKey() + withContext(Dispatchers.IO) { + kotlin.runCatching { + var str = sortUrl + if (sortUrl?.startsWith("", false) == true + || sortUrl?.startsWith("@js:", false) == true + ) { + str = aCache.getAsString(sortUrlsKey) + if (str.isNullOrBlank()) { + val jsStr = if (sortUrl!!.startsWith("@")) { + sortUrl!!.substring(4) + } else { + sortUrl!!.substring(4, sortUrl!!.lastIndexOf("<")) + } + str = runScriptWithContext { + evalJS(jsStr).toString() + } + aCache.put(sortUrlsKey, str) + } + } + str?.split("(&&|\n)+".toRegex())?.forEach { sort -> + val name = sort.substringBefore("::") + val url = sort.substringAfter("::", "") + if (url.isNotEmpty()) { + if (url.startsWith("{{")) { + add(Pair(name, url)) + } else { + add(Pair(name, NetworkUtils.getAbsoluteURL(sourceUrl, url))) + } + } + } + if (isEmpty()) { + add(Pair("", sourceUrl)) + } + } + } + } +} + +suspend fun RssSource.removeSortCache() { + withContext(Dispatchers.IO) { + aCache.remove(getSortUrlsKey()) + } +} diff --git a/app/src/main/java/io/legado/app/help/source/SourceHelp.kt b/app/src/main/java/io/legado/app/help/source/SourceHelp.kt new file mode 100755 index 000000000..6efd4df24 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/source/SourceHelp.kt @@ -0,0 +1,178 @@ +package io.legado.app.help.source + +import io.legado.app.constant.SourceType +import io.legado.app.data.appDb +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.RssSource +import io.legado.app.help.AppCacheManager +import io.legado.app.help.config.SourceConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.AudioPlay +import io.legado.app.model.ReadBook +import io.legado.app.model.ReadManga +import io.legado.app.utils.EncoderUtils +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +object SourceHelp { + + private val list18Plus by lazy { + try { + return@lazy String(appCtx.assets.open("18PlusList.txt").readBytes()) + .splitNotBlank("\n").map { + EncoderUtils.base64Decode(it) + }.toHashSet() + } catch (_: Exception) { + return@lazy emptySet() + } + } + + fun getSource(key: String?): BaseSource? { + key ?: return null + if (ReadBook.bookSource?.bookSourceUrl == key) { + return ReadBook.bookSource + } else if (AudioPlay.bookSource?.bookSourceUrl == key) { + return AudioPlay.bookSource + } else if (ReadManga.bookSource?.bookSourceUrl == key) { + return ReadManga.bookSource + } + return appDb.bookSourceDao.getBookSource(key) + ?: appDb.rssSourceDao.getByKey(key) + } + + fun getSource(key: String?, @SourceType.Type type: Int): BaseSource? { + key ?: return null + return when (type) { + SourceType.book -> appDb.bookSourceDao.getBookSource(key) + SourceType.rss -> appDb.rssSourceDao.getByKey(key) + else -> null + } + } + + fun deleteSource(key: String, @SourceType.Type type: Int) { + when (type) { + SourceType.book -> deleteBookSource(key) + SourceType.rss -> deleteRssSource(key) + } + } + + fun deleteBookSourceParts(sources: List) { + appDb.runInTransaction { + sources.forEach { + deleteBookSourceInternal(it.bookSourceUrl) + } + } + AppCacheManager.clearSourceVariables() + } + + fun deleteBookSources(sources: List) { + appDb.runInTransaction { + sources.forEach { + deleteBookSourceInternal(it.bookSourceUrl) + } + } + AppCacheManager.clearSourceVariables() + } + + private fun deleteBookSourceInternal(key: String) { + appDb.bookSourceDao.delete(key) + appDb.cacheDao.deleteSourceVariables(key) + SourceConfig.removeSource(key) + } + + fun deleteBookSource(key: String) { + deleteBookSourceInternal(key) + AppCacheManager.clearSourceVariables() + } + + fun deleteRssSources(sources: List) { + appDb.runInTransaction { + sources.forEach { + deleteRssSourceInternal(it.sourceUrl) + } + } + AppCacheManager.clearSourceVariables() + } + + private fun deleteRssSourceInternal(key: String) { + appDb.rssSourceDao.delete(key) + appDb.rssArticleDao.delete(key) + appDb.cacheDao.deleteSourceVariables(key) + } + + fun deleteRssSource(key: String) { + deleteRssSourceInternal(key) + AppCacheManager.clearSourceVariables() + } + + fun enableSource(key: String, @SourceType.Type type: Int, enable: Boolean) { + when (type) { + SourceType.book -> appDb.bookSourceDao.enable(key, enable) + SourceType.rss -> appDb.rssSourceDao.enable(key, enable) + } + } + + fun insertRssSource(vararg rssSources: RssSource) { + val rssSourcesGroup = rssSources.groupBy { + is18Plus(it.sourceUrl) + } + rssSourcesGroup[true]?.forEach { + appCtx.toastOnUi("${it.sourceName}是18+网址,禁止导入.") + } + rssSourcesGroup[false]?.let { + appDb.rssSourceDao.insert(*it.toTypedArray()) + } + } + + fun insertBookSource(vararg bookSources: BookSource) { + val bookSourcesGroup = bookSources.groupBy { + is18Plus(it.bookSourceUrl) + } + bookSourcesGroup[true]?.forEach { + appCtx.toastOnUi("${it.bookSourceName}是18+网址,禁止导入.") + } + bookSourcesGroup[false]?.let { + appDb.bookSourceDao.insert(*it.toTypedArray()) + } + Coroutine.async { + adjustSortNumber() + } + } + + private fun is18Plus(url: String?): Boolean { + if (list18Plus.isEmpty()) { + return false + } + url ?: return false + val baseUrl = NetworkUtils.getBaseUrl(url) ?: return false + kotlin.runCatching { + val host = baseUrl.split("//", ".").let { + if (it.size > 2) "${it[it.lastIndex - 1]}.${it.last()}" else return false + } + return list18Plus.contains(host) + } + return false + } + + /** + * 调整排序序号 + */ + fun adjustSortNumber() { + if ( + appDb.bookSourceDao.maxOrder > 99999 + || appDb.bookSourceDao.minOrder < -99999 + || appDb.bookSourceDao.hasDuplicateOrder + ) { + val sources = appDb.bookSourceDao.allPart + sources.forEachIndexed { index, bookSource -> + bookSource.customOrder = index + } + appDb.bookSourceDao.upOrder(sources) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/source/SourceVerificationHelp.kt b/app/src/main/java/io/legado/app/help/source/SourceVerificationHelp.kt new file mode 100755 index 000000000..e2815253b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/source/SourceVerificationHelp.kt @@ -0,0 +1,120 @@ +package io.legado.app.help.source + +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.BaseSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.CacheManager +import io.legado.app.help.IntentData +import io.legado.app.ui.association.VerificationCodeActivity +import io.legado.app.ui.browser.WebViewActivity +import io.legado.app.utils.isMainThread +import io.legado.app.utils.startActivity +import splitties.init.appCtx +import java.util.concurrent.locks.LockSupport +import kotlin.time.Duration.Companion.minutes + +/** + * 源验证 + */ +object SourceVerificationHelp { + + private val waitTime = 1.minutes.inWholeNanoseconds + + private fun getVerificationResultKey(source: BaseSource) = + getVerificationResultKey(source.getKey()) + + private fun getVerificationResultKey(sourceKey: String) = "${sourceKey}_verificationResult" + + /** + * 获取书源验证结果 + * 图片验证码 防爬 滑动验证码 点击字符 等等 + */ + @Synchronized + fun getVerificationResult( + source: BaseSource?, + url: String, + title: String, + useBrowser: Boolean, + refetchAfterSuccess: Boolean = true + ): String { + source + ?: throw NoStackTraceException("getVerificationResult parameter source cannot be null") + require(url.length < 64 * 1024) { "getVerificationResult parameter url too long" } + check(!isMainThread) { "getVerificationResult must be called on a background thread" } + + clearResult(source.getKey()) + + if (!useBrowser) { + appCtx.startActivity { + putExtra("imageUrl", url) + putExtra("sourceOrigin", source.getKey()) + putExtra("sourceName", source.getTag()) + putExtra("sourceType", source.getSourceType()) + IntentData.put(getVerificationResultKey(source), Thread.currentThread()) + } + } else { + startBrowser(source, url, title, true, refetchAfterSuccess) + } + + var waitUserInput = false + while (getResult(source.getKey()) == null) { + if (!waitUserInput) { + AppLog.putDebug("等待返回验证结果...") + waitUserInput = true + } + LockSupport.parkNanos(this, waitTime) + } + + val result = getResult(source.getKey())!! + clearResult(source.getKey()) + result.ifBlank { + throw NoStackTraceException("验证结果为空") + } + + return result + } + + /** + * 启动内置浏览器 + * @param saveResult 保存网页源代码到数据库 + */ + fun startBrowser( + source: BaseSource?, + url: String, + title: String, + saveResult: Boolean? = false, + refetchAfterSuccess: Boolean? = true + ) { + source ?: throw NoStackTraceException("startBrowser parameter source cannot be null") + require(url.length < 64 * 1024) { "startBrowser parameter url too long" } + appCtx.startActivity { + putExtra("title", title) + putExtra("url", url) + putExtra("sourceOrigin", source.getKey()) + putExtra("sourceName", source.getTag()) + putExtra("sourceType", source.getSourceType()) + putExtra("sourceVerificationEnable", saveResult) + putExtra("refetchAfterSuccess", refetchAfterSuccess) + IntentData.put(getVerificationResultKey(source), Thread.currentThread()) + } + } + + + fun checkResult(sourceKey: String) { + getResult(sourceKey) ?: setResult(sourceKey, "") + val thread = IntentData.get(getVerificationResultKey(sourceKey)) + LockSupport.unpark(thread) + } + + fun setResult(sourceKey: String, result: String?) { + CacheManager.putMemory(getVerificationResultKey(sourceKey), result ?: "") + } + + fun getResult(sourceKey: String): String? { + return CacheManager.getFromMemory(getVerificationResultKey(sourceKey)) as? String + } + + fun clearResult(sourceKey: String) { + CacheManager.delete(getVerificationResultKey(sourceKey)) + } +} diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt new file mode 100755 index 000000000..7e91e06b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -0,0 +1,297 @@ +package io.legado.app.help.storage + +import android.content.Context +import android.net.Uri +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.BookCover +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.LogUtils +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.createFolderIfNotExist +import io.legado.app.utils.defaultSharedPreferences +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getFile +import io.legado.app.utils.getSharedPreferences +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.normalizeFileName +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.outputStream +import io.legado.app.utils.writeToOutputStream +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.concurrent.TimeUnit + +/** + * 备份 + */ +object Backup { + + val backupPath: String by lazy { + appCtx.filesDir.getFile("backup").createFolderIfNotExist().absolutePath + } + val zipFilePath = "${appCtx.externalFiles.absolutePath}${File.separator}tmp_backup.zip" + + private const val TAG = "Backup" + + private val mutex = Mutex() + + private val backupFileNames by lazy { + arrayOf( + "bookshelf.json", + "bookmark.json", + "bookGroup.json", + "bookSource.json", + "rssSources.json", + "rssStar.json", + "replaceRule.json", + "readRecord.json", + "searchHistory.json", + "sourceSub.json", + "txtTocRule.json", + "httpTTS.json", + "keyboardAssists.json", + "dictRule.json", + "servers.json", + DirectLinkUpload.ruleFileName, + ReadBookConfig.configFileName, + ReadBookConfig.shareConfigFileName, + ThemeConfig.configFileName, + BookCover.configFileName, + "config.xml" + ) + } + + private fun getNowZipFileName(): String { + val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + .format(Date(System.currentTimeMillis())) + val deviceName = AppConfig.webDavDeviceName + return if (deviceName?.isNotBlank() == true) { + "backup${backupDate}-${deviceName}.zip" + } else { + "backup${backupDate}.zip" + }.normalizeFileName() + } + + private fun shouldBackup(): Boolean { + val lastBackup = LocalConfig.lastBackup + return lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis() + } + + fun autoBack(context: Context) { + if (shouldBackup()) { + Coroutine.async { + mutex.withLock { + if (shouldBackup()) { + val backupZipFileName = getNowZipFileName() + if (!AppWebDav.hasBackUp(backupZipFileName)) { + backup(context, AppConfig.backupPath) + } else { + LocalConfig.lastBackup = System.currentTimeMillis() + } + } + } + }.onError { + AppLog.put("自动备份失败\n${it.localizedMessage}") + } + } + } + + suspend fun backupLocked(context: Context, path: String?) { + mutex.withLock { + withContext(IO) { + backup(context, path) + } + } + } + + private suspend fun backup(context: Context, path: String?) { + LogUtils.d(TAG, "开始备份 path:$path") + LocalConfig.lastBackup = System.currentTimeMillis() + val aes = BackupAES() + FileUtils.delete(backupPath) + writeListToJson(appDb.bookDao.all, "bookshelf.json", backupPath) + writeListToJson(appDb.bookmarkDao.all, "bookmark.json", backupPath) + writeListToJson(appDb.bookGroupDao.all, "bookGroup.json", backupPath) + writeListToJson(appDb.bookSourceDao.all, "bookSource.json", backupPath) + writeListToJson(appDb.rssSourceDao.all, "rssSources.json", backupPath) + writeListToJson(appDb.rssStarDao.all, "rssStar.json", backupPath) + writeListToJson(appDb.replaceRuleDao.all, "replaceRule.json", backupPath) + writeListToJson(appDb.readRecordDao.all, "readRecord.json", backupPath) + writeListToJson(appDb.searchKeywordDao.all, "searchHistory.json", backupPath) + writeListToJson(appDb.ruleSubDao.all, "sourceSub.json", backupPath) + writeListToJson(appDb.txtTocRuleDao.all, "txtTocRule.json", backupPath) + writeListToJson(appDb.httpTTSDao.all, "httpTTS.json", backupPath) + writeListToJson(appDb.keyboardAssistsDao.all, "keyboardAssists.json", backupPath) + writeListToJson(appDb.dictRuleDao.all, "dictRule.json", backupPath) + GSON.toJson(appDb.serverDao.all).let { json -> + aes.runCatching { + encryptBase64(json) + }.getOrDefault(json).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + "servers.json") + .writeText(it) + } + } + currentCoroutineContext().ensureActive() + GSON.toJson(ReadBookConfig.configList).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName) + .writeText(it) + } + GSON.toJson(ReadBookConfig.shareConfig).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName) + .writeText(it) + } + GSON.toJson(ThemeConfig.configList).let { + FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfig.configFileName) + .writeText(it) + } + DirectLinkUpload.getConfig()?.let { + FileUtils.createFileIfNotExist(backupPath + File.separator + DirectLinkUpload.ruleFileName) + .writeText(GSON.toJson(it)) + } + BookCover.getConfig()?.let { + FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName) + .writeText(GSON.toJson(it)) + } + currentCoroutineContext().ensureActive() + appCtx.getSharedPreferences(backupPath, "config")?.let { sp -> + val edit = sp.edit() + appCtx.defaultSharedPreferences.all.forEach { (key, value) -> + if (BackupConfig.keyIsNotIgnore(key)) { + when (key) { + PreferKey.webDavPassword -> { + edit.putString(key, aes.runCatching { + encryptBase64(value.toString()) + }.getOrDefault(value.toString())) + } + + else -> when (value) { + is Int -> edit.putInt(key, value) + is Boolean -> edit.putBoolean(key, value) + is Long -> edit.putLong(key, value) + is Float -> edit.putFloat(key, value) + is String -> edit.putString(key, value) + } + } + } + } + edit.commit() + } + currentCoroutineContext().ensureActive() + val zipFileName = getNowZipFileName() + val paths = arrayListOf(*backupFileNames) + for (i in 0 until paths.size) { + paths[i] = backupPath + File.separator + paths[i] + } + FileUtils.delete(zipFilePath) + FileUtils.delete(zipFilePath.replace("tmp_", "")) + val backupFileName = if (AppConfig.onlyLatestBackup) { + "backup.zip" + } else { + zipFileName + } + if (ZipUtils.zipFiles(paths, zipFilePath)) { + when { + path.isNullOrBlank() -> { + copyBackup(context.getExternalFilesDir(null)!!, backupFileName) + } + + path.isContentScheme() -> { + copyBackup(context, path.toUri(), backupFileName) + } + + else -> { + copyBackup(File(path), backupFileName) + } + } + try { + AppWebDav.backUpWebDav(zipFileName) + } catch (e: Exception) { + AppLog.put("上传备份至webdav失败\n$e", e) + } + } + FileUtils.delete(backupPath) + FileUtils.delete(zipFilePath) + currentCoroutineContext().ensureActive() + ReadBookConfig.getAllPicBgStr().map { + if (it.contains(File.separator)) { + File(it) + } else { + appCtx.externalFiles.getFile("bg", it) + } + }.let { + AppWebDav.upBgs(it.toTypedArray()) + } + } + + private suspend fun writeListToJson(list: List, fileName: String, path: String) { + currentCoroutineContext().ensureActive() + withContext(IO) { + if (list.isNotEmpty()) { + LogUtils.d(TAG, "阅读备份 $fileName 列表大小 ${list.size}") + val file = FileUtils.createFileIfNotExist(path + File.separator + fileName) + file.outputStream().buffered().use { + GSON.writeToOutputStream(it, list) + } + LogUtils.d(TAG, "阅读备份 $fileName 写入大小 ${file.length()}") + } else { + LogUtils.d(TAG, "阅读备份 $fileName 列表为空") + } + } + } + + @Throws(Exception::class) + @Suppress("SameParameterValue") + private fun copyBackup(context: Context, uri: Uri, fileName: String) { + val treeDoc = DocumentFile.fromTreeUri(context, uri)!! + treeDoc.findFile(fileName)?.delete() + val fileDoc = treeDoc.createFile("", fileName) + ?: throw NoStackTraceException("创建文件失败") + val outputS = fileDoc.openOutputStream() + ?: throw NoStackTraceException("打开OutputStream失败") + outputS.use { + FileInputStream(zipFilePath).use { inputS -> + inputS.copyTo(outputS) + } + } + } + + @Throws(Exception::class) + @Suppress("SameParameterValue") + private fun copyBackup(rootFile: File, fileName: String) { + FileInputStream(File(zipFilePath)).use { inputS -> + val file = FileUtils.createFileIfNotExist(rootFile, fileName) + FileOutputStream(file).use { outputS -> + inputS.copyTo(outputS) + } + } + } + + fun clearCache() { + FileUtils.delete(backupPath) + FileUtils.delete(zipFilePath) + } +} diff --git a/app/src/main/java/io/legado/app/help/storage/BackupAES.kt b/app/src/main/java/io/legado/app/help/storage/BackupAES.kt new file mode 100755 index 000000000..f420e2c23 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/BackupAES.kt @@ -0,0 +1,9 @@ +package io.legado.app.help.storage + +import cn.hutool.crypto.symmetric.AES +import io.legado.app.help.config.LocalConfig +import io.legado.app.utils.MD5Utils + +class BackupAES : AES( + MD5Utils.md5Encode(LocalConfig.password ?: "").encodeToByteArray(0, 16) +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/BackupConfig.kt b/app/src/main/java/io/legado/app/help/storage/BackupConfig.kt new file mode 100755 index 000000000..3c0ad84ef --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/BackupConfig.kt @@ -0,0 +1,145 @@ +package io.legado.app.help.storage + +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import splitties.init.appCtx + +/** + * 备份配置 + */ +@Suppress("ConstPropertyName") +object BackupConfig { + + private val ignoreConfigPath = FileUtils.getPath(appCtx.filesDir, "restoreIgnore.json") + val ignoreConfig: HashMap by lazy { + val file = FileUtils.createFileIfNotExist(ignoreConfigPath) + val json = file.readText() + GSON.fromJsonObject>(json).getOrNull() ?: hashMapOf() + } + + private const val readConfigKey = "readConfig" + private const val themeConfigKey = "themeConfig" + private const val coverConfigKey = "coverConfig" + private const val localBookKey = "localBook" + + //配置忽略key + val ignoreKeys = arrayOf( + readConfigKey, + PreferKey.themeMode, + themeConfigKey, + coverConfigKey, + PreferKey.bookshelfLayout, + PreferKey.showRss, + PreferKey.threadCount, + localBookKey + ) + + //配置忽略标题 + val ignoreTitle = arrayOf( + appCtx.getString(R.string.read_config), + appCtx.getString(R.string.theme_mode), + appCtx.getString(R.string.theme_config), + appCtx.getString(R.string.cover_config), + appCtx.getString(R.string.bookshelf_layout), + appCtx.getString(R.string.show_rss), + appCtx.getString(R.string.thread_count), + appCtx.getString(R.string.local_book) + ) + + //自动忽略keys + private val ignorePrefKeys = arrayOf( + PreferKey.defaultCover, + PreferKey.defaultCoverDark, + PreferKey.backupPath, + PreferKey.defaultBookTreeUri, + PreferKey.webDavDeviceName, + PreferKey.launcherIcon, + PreferKey.bitmapCacheSize, + PreferKey.webServiceWakeLock, + PreferKey.readAloudWakeLock, + PreferKey.audioPlayWakeLock + ) + + //阅读配置 + private val readPrefKeys = arrayOf( + PreferKey.readStyleSelect, + PreferKey.comicStyleSelect, + PreferKey.shareLayout, + PreferKey.hideStatusBar, + PreferKey.hideNavigationBar, + PreferKey.autoReadSpeed, + PreferKey.clickActionTL, + PreferKey.clickActionTC, + PreferKey.clickActionTR, + PreferKey.clickActionML, + PreferKey.clickActionMC, + PreferKey.clickActionMR, + PreferKey.clickActionBL, + PreferKey.clickActionBC, + PreferKey.clickActionBR + ) + + private val themePrefKeys = arrayOf( + PreferKey.cPrimary, + PreferKey.cAccent, + PreferKey.cBackground, + PreferKey.cBBackground, + PreferKey.bgImage, + PreferKey.bgImageBlurring, + PreferKey.cNPrimary, + PreferKey.cNAccent, + PreferKey.cNBackground, + PreferKey.cNBBackground, + PreferKey.bgImageN, + PreferKey.bgImageNBlurring + ) + + private val coverPrefKeys = arrayOf( + PreferKey.useDefaultCover, + PreferKey.loadCoverOnlyWifi, + PreferKey.coverShowName, + PreferKey.coverShowAuthor, + PreferKey.coverShowNameN, + PreferKey.coverShowAuthorN + ) + + fun keyIsNotIgnore(key: String): Boolean { + return when { + ignorePrefKeys.contains(key) -> false + ignoreReadConfig && readPrefKeys.contains(key) -> false + ignoreThemeConfig && themePrefKeys.contains(key) -> false + ignoreCoverConfig && coverPrefKeys.contains(key) -> false + PreferKey.themeMode == key && ignoreThemeMode -> false + PreferKey.bookshelfLayout == key && ignoreBookshelfLayout -> false + PreferKey.showRss == key && ignoreShowRss -> false + PreferKey.threadCount == key && ignoreThreadCount -> false + else -> true + } + } + + val ignoreReadConfig: Boolean + get() = ignoreConfig[readConfigKey] == true + private val ignoreThemeMode: Boolean + get() = ignoreConfig[PreferKey.themeMode] == true + private val ignoreThemeConfig: Boolean + get() = ignoreConfig[themeConfigKey] == true + private val ignoreCoverConfig: Boolean + get() = ignoreConfig[coverConfigKey] == true + private val ignoreBookshelfLayout: Boolean + get() = ignoreConfig[PreferKey.bookshelfLayout] == true + private val ignoreShowRss: Boolean + get() = ignoreConfig[PreferKey.showRss] == true + private val ignoreThreadCount: Boolean + get() = ignoreConfig[PreferKey.threadCount] == true + val ignoreLocalBook: Boolean + get() = ignoreConfig[localBookKey] == true + + fun saveIgnoreConfig() { + val json = GSON.toJson(ignoreConfig) + FileUtils.createFileIfNotExist(ignoreConfigPath).writeText(json) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt new file mode 100755 index 000000000..5689c84e5 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt @@ -0,0 +1,373 @@ +package io.legado.app.help.storage + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.jayway.jsonpath.DocumentContext +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.constant.BookSourceType +import io.legado.app.constant.BookType +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.rule.* +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.ReplaceAnalyzer +import io.legado.app.utils.* +import splitties.init.appCtx +import java.io.File +import java.util.regex.Pattern + +object ImportOldData { + + @Suppress("RegExpRedundantEscape") + private val headerPattern = Pattern.compile("@Header:\\{.+?\\}", Pattern.CASE_INSENSITIVE) + @Suppress("RegExpRedundantEscape") + private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE) + + fun importUri(context: Context, uri: Uri) { + if (uri.isContentScheme()) { + DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc -> + when (doc.name) { + "myBookShelf.json" -> + kotlin.runCatching { + doc.uri.readText(context).let { json -> + val importCount = importOldBookshelf(json) + context.toastOnUi("成功导入书架${importCount}") + } + }.onFailure { + context.toastOnUi("导入书架失败\n${it.localizedMessage}") + } + "myBookSource.json" -> + kotlin.runCatching { + doc.uri.readText(context).let { json -> + val importCount = importOldSource(json) + context.toastOnUi("成功导入书源${importCount}") + } + }.onFailure { + context.toastOnUi("导入源失败\n${it.localizedMessage}") + } + "myBookReplaceRule.json" -> + kotlin.runCatching { + doc.uri.readText(context).let { json -> + val importCount = importOldReplaceRule(json) + context.toastOnUi("成功导入替换规则${importCount}") + } + }.onFailure { + context.toastOnUi("导入替换规则失败\n${it.localizedMessage}") + } + } + } + } else { + uri.path?.let { path -> + val file = File(path) + kotlin.runCatching {// 导入书架 + val shelfFile = + FileUtils.createFileIfNotExist(file, "myBookShelf.json") + val json = shelfFile.readText() + val importCount = importOldBookshelf(json) + context.toastOnUi("成功导入书架${importCount}") + }.onFailure { + context.toastOnUi("导入书架失败\n${it.localizedMessage}") + } + + kotlin.runCatching {// Book source + val sourceFile = + file.getFile("myBookSource.json") + val json = sourceFile.readText() + val importCount = importOldSource(json) + context.toastOnUi("成功导入书源${importCount}") + }.onFailure { + context.toastOnUi("导入源失败\n${it.localizedMessage}") + } + + kotlin.runCatching {// Replace rules + val ruleFile = file.getFile("myBookReplaceRule.json") + if (ruleFile.exists()) { + val json = ruleFile.readText() + val importCount = importOldReplaceRule(json) + context.toastOnUi("成功导入替换规则${importCount}") + } else { + context.toastOnUi("未找到替换规则") + } + }.onFailure { + context.toastOnUi("导入替换规则失败\n${it.localizedMessage}") + } + } + } + } + + private fun importOldBookshelf(json: String): Int { + val books = fromOldBooks(json) + appDb.bookDao.insert(*books.toTypedArray()) + return books.size + } + + fun importOldSource(json: String): Int { + val sources = fromOldBookSources(json) + appDb.bookSourceDao.insert(*sources.toTypedArray()) + return sources.size + } + + private fun importOldReplaceRule(json: String): Int { + val rules = ReplaceAnalyzer.jsonToReplaceRules(json).getOrNull() + rules?.let { + appDb.replaceRuleDao.insert(*rules.toTypedArray()) + return rules.size + } + return 0 + } + + private fun fromOldBooks(json: String): List { + val books = mutableListOf() + val items: List> = jsonPath.parse(json).read("$") + val existingBooks = appDb.bookDao.allBookUrls.toSet() + for (item in items) { + val jsonItem = jsonPath.parse(item) + val book = Book() + book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" + if (book.bookUrl.isBlank()) continue + book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" + if (book.bookUrl in existingBooks) { + DebugLog.d(javaClass.name, "Found existing book: " + book.name) + continue + } + book.origin = jsonItem.readString("$.tag") ?: "" + book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" + book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" + val local = if (book.origin == "loc_book") BookType.local else 0 + val isAudio = jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO" + book.type = local or if (isAudio) BookType.audio else BookType.text + book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl + book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") + book.customCoverUrl = jsonItem.readString("$.customCoverPath") + book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 + book.canUpdate = jsonItem.readBool("$.allowUpdate") == true + book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 + book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 + book.durChapterTitle = jsonItem.readString("$.durChapterName") + book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 + book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 + book.intro = jsonItem.readString("$.bookInfoBean.introduce") + book.latestChapterTitle = jsonItem.readString("$.lastChapterName") + book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 + book.order = jsonItem.readInt("$.serialNumber") ?: 0 + book.variable = jsonItem.readString("$.variable") + book.setUseReplaceRule(jsonItem.readBool("$.useReplaceRule") == true) + books.add(book) + } + return books + } + + private fun fromOldBookSources(json: String): MutableList { + val sources = mutableListOf() + val items: List> = jsonPath.parse(json).read("$") + for (item in items) { + val jsonItem = jsonPath.parse(item) + val source = fromOldBookSource(jsonItem) + sources.add(source) + } + return sources + } + + fun fromOldBookSource(jsonItem: DocumentContext): BookSource { + val source = BookSource() + return source.apply { + bookSourceUrl = jsonItem.readString("bookSourceUrl") + ?: throw NoStackTraceException(appCtx.getString(R.string.wrong_format)) + bookSourceName = jsonItem.readString("bookSourceName") ?: "" + bookSourceGroup = jsonItem.readString("bookSourceGroup") + loginUrl = jsonItem.readString("loginUrl") + loginUi = jsonItem.readString("loginUi") + loginCheckJs = jsonItem.readString("loginCheckJs") + coverDecodeJs = jsonItem.readString("coverDecodeJs") + bookSourceComment = jsonItem.readString("bookSourceComment") ?: "" + bookUrlPattern = jsonItem.readString("ruleBookUrlPattern") + customOrder = jsonItem.readInt("serialNumber") ?: 0 + header = uaToHeader(jsonItem.readString("httpUserAgent")) + searchUrl = toNewUrl(jsonItem.readString("ruleSearchUrl")) + exploreUrl = toNewUrls(jsonItem.readString("ruleFindUrl")) + bookSourceType = + if (jsonItem.readString("bookSourceType") == "AUDIO") BookSourceType.audio else BookSourceType.default + enabled = jsonItem.readBool("enable") ?: true + if (exploreUrl.isNullOrBlank()) { + enabledExplore = false + } + ruleSearch = SearchRule( + bookList = toNewRule(jsonItem.readString("ruleSearchList")), + name = toNewRule(jsonItem.readString("ruleSearchName")), + author = toNewRule(jsonItem.readString("ruleSearchAuthor")), + intro = toNewRule(jsonItem.readString("ruleSearchIntroduce")), + kind = toNewRule(jsonItem.readString("ruleSearchKind")), + bookUrl = toNewRule(jsonItem.readString("ruleSearchNoteUrl")), + coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")), + lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter")) + ) + ruleExplore = ExploreRule( + bookList = toNewRule(jsonItem.readString("ruleFindList")), + name = toNewRule(jsonItem.readString("ruleFindName")), + author = toNewRule(jsonItem.readString("ruleFindAuthor")), + intro = toNewRule(jsonItem.readString("ruleFindIntroduce")), + kind = toNewRule(jsonItem.readString("ruleFindKind")), + bookUrl = toNewRule(jsonItem.readString("ruleFindNoteUrl")), + coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")), + lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter")) + ) + ruleBookInfo = BookInfoRule( + init = toNewRule(jsonItem.readString("ruleBookInfoInit")), + name = toNewRule(jsonItem.readString("ruleBookName")), + author = toNewRule(jsonItem.readString("ruleBookAuthor")), + intro = toNewRule(jsonItem.readString("ruleIntroduce")), + kind = toNewRule(jsonItem.readString("ruleBookKind")), + coverUrl = toNewRule(jsonItem.readString("ruleCoverUrl")), + lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")), + tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl")) + ) + ruleToc = TocRule( + chapterList = toNewRule(jsonItem.readString("ruleChapterList")), + chapterName = toNewRule(jsonItem.readString("ruleChapterName")), + chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")), + nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext")) + ) + var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: "" + if (content.startsWith("$") && !content.startsWith("$.")) { + content = content.substring(1) + } + ruleContent = ContentRule( + content = content, + replaceRegex = toNewRule(jsonItem.readString("ruleBookContentReplace")), + nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext")) + ) + } + } + + + // default规则适配 + // #正则#替换内容 替换成 ##正则##替换内容 + // | 替换成 || + // & 替换成 && + private fun toNewRule(oldRule: String?): String? { + if (oldRule.isNullOrBlank()) return null + var newRule = oldRule + var reverse = false + var allinone = false + if (oldRule.startsWith("-")) { + reverse = true + newRule = oldRule.substring(1) + } + if (newRule.startsWith("+")) { + allinone = true + newRule = newRule.substring(1) + } + if (!newRule.startsWith("@CSS:", true) && + !newRule.startsWith("@XPath:", true) && + !newRule.startsWith("//") && + !newRule.startsWith("##") && + !newRule.startsWith(":") && + !newRule.contains("@js:", true) && + !newRule.contains("", true) + ) { + if (newRule.contains("#") && !newRule.contains("##")) { + newRule = oldRule.replace("#", "##") + } + if (newRule.contains("|") && !newRule.contains("||")) { + if (newRule.contains("##")) { + val list = newRule.split("##") + if (list[0].contains("|")) { + newRule = list[0].replace("|", "||") + for (i in 1 until list.size) { + newRule += "##" + list[i] + } + } + } else { + newRule = newRule.replace("|", "||") + } + } + if (newRule.contains("&") + && !newRule.contains("&&") + && !newRule.contains("http") + && !newRule.startsWith("/") + ) { + newRule = newRule.replace("&", "&&") + } + } + if (allinone) { + newRule = "+$newRule" + } + if (reverse) { + newRule = "-$newRule" + } + return newRule + } + + private fun toNewUrls(oldUrls: String?): String? { + if (oldUrls.isNullOrBlank()) return null + if (oldUrls.startsWith("@js:") || oldUrls.startsWith("")) { + return oldUrls + } + if (!oldUrls.contains("\n") && !oldUrls.contains("&&")) { + return toNewUrl(oldUrls) + } + val urls = oldUrls.split("(&&|\r?\n)+".toRegex()) + return urls.map { + toNewUrl(it)?.replace("\n\\s*".toRegex(), "") + }.joinToString("\n") + } + + private fun toNewUrl(oldUrl: String?): String? { + if (oldUrl.isNullOrBlank()) return null + var url: String = oldUrl + if (oldUrl.startsWith("", true)) { + url = url.replace("=searchKey", "={{key}}") + .replace("=searchPage", "={{page}}") + return url + } + val map = HashMap() + var mather = headerPattern.matcher(url) + if (mather.find()) { + val header = mather.group() + url = url.replace(header, "") + map["headers"] = header.substring(8) + } + var urlList = url.split("|") + url = urlList[0] + if (urlList.size > 1) { + map["charset"] = urlList[1].split("=")[1] + } + mather = jsPattern.matcher(url) + val jsList = arrayListOf() + while (mather.find()) { + jsList.add(mather.group()) + url = url.replace(jsList.last(), "$${jsList.size - 1}") + } + url = url.replace("{", "<").replace("}", ">") + url = url.replace("searchKey", "{{key}}") + url = url.replace("".toRegex(), "{{page$1}}") + .replace("searchPage([-+]1)".toRegex(), "{{page$1}}") + .replace("searchPage", "{{page}}") + for ((index, item) in jsList.withIndex()) { + url = url.replace( + "$$index", + item.replace("searchKey", "key").replace("searchPage", "page") + ) + } + urlList = url.split("@") + url = urlList[0] + if (urlList.size > 1) { + map["method"] = "POST" + map["body"] = urlList[1] + } + if (map.size > 0) { + url += "," + GSON.toJson(map) + } + return url + } + + private fun uaToHeader(ua: String?): String? { + if (ua.isNullOrEmpty()) return null + val map = mapOf(Pair(AppConst.UA_NAME, ua)) + return GSON.toJson(map) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt new file mode 100755 index 000000000..f2116eac6 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -0,0 +1,315 @@ +package io.legado.app.help.storage + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import io.legado.app.BuildConfig +import io.legado.app.R +import io.legado.app.constant.AppConst.androidId +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.Bookmark +import io.legado.app.data.entities.DictRule +import io.legado.app.data.entities.HttpTTS +import io.legado.app.data.entities.KeyboardAssist +import io.legado.app.data.entities.ReadRecord +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.data.entities.RssSource +import io.legado.app.data.entities.RssStar +import io.legado.app.data.entities.RuleSub +import io.legado.app.data.entities.SearchKeyword +import io.legado.app.data.entities.Server +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.LauncherIconHelp +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.upType +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.model.BookCover +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.ACache +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.LogUtils +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.defaultSharedPreferences +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.getPrefString +import io.legado.app.utils.getSharedPreferences +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.openInputStream +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.io.File +import java.io.FileInputStream + +/** + * 恢复 + */ +object Restore { + + private val mutex = Mutex() + + private const val TAG = "Restore" + + suspend fun restore(context: Context, uri: Uri) { + LogUtils.d(TAG, "开始恢复备份 uri:$uri") + kotlin.runCatching { + FileUtils.delete(Backup.backupPath) + if (uri.isContentScheme()) { + DocumentFile.fromSingleUri(context, uri)!!.openInputStream()!!.use { + ZipUtils.unZipToPath(it, Backup.backupPath) + } + } else { + ZipUtils.unZipToPath(File(uri.path!!), Backup.backupPath) + } + }.onFailure { + AppLog.put("复制解压文件出错\n${it.localizedMessage}", it) + return + } + kotlin.runCatching { + restoreLocked(Backup.backupPath) + LocalConfig.lastBackup = System.currentTimeMillis() + }.onFailure { + appCtx.toastOnUi("恢复备份出错\n${it.localizedMessage}") + AppLog.put("恢复备份出错\n${it.localizedMessage}", it) + } + } + + suspend fun restoreLocked(path: String) { + mutex.withLock { + restore(path) + } + } + + private suspend fun restore(path: String) { + val aes = BackupAES() + fileToListT(path, "bookshelf.json")?.let { + it.forEach { book -> + book.upType() + } + it.filter { book -> book.isLocal } + .forEach { book -> + book.coverUrl = LocalBook.getCoverPath(book) + } + val newBooks = arrayListOf() + val ignoreLocalBook = BackupConfig.ignoreLocalBook + it.forEach { book -> + if (ignoreLocalBook && book.isLocal) { + return@forEach + } + if (appDb.bookDao.has(book.bookUrl)) { + try { + appDb.bookDao.update(book) + } catch (_: SQLiteConstraintException) { + appDb.bookDao.insert(book) + } + } else { + newBooks.add(book) + } + } + appDb.bookDao.insert(*newBooks.toTypedArray()) + } + fileToListT(path, "bookmark.json")?.let { + appDb.bookmarkDao.insert(*it.toTypedArray()) + } + fileToListT(path, "bookGroup.json")?.let { + appDb.bookGroupDao.insert(*it.toTypedArray()) + } + fileToListT(path, "bookSource.json")?.let { + appDb.bookSourceDao.insert(*it.toTypedArray()) + } ?: run { + val bookSourceFile = File(path, "bookSource.json") + if (bookSourceFile.exists()) { + val json = bookSourceFile.readText() + ImportOldData.importOldSource(json) + } + } + fileToListT(path, "rssSources.json")?.let { + appDb.rssSourceDao.insert(*it.toTypedArray()) + } + fileToListT(path, "rssStar.json")?.let { + appDb.rssStarDao.insert(*it.toTypedArray()) + } + fileToListT(path, "replaceRule.json")?.let { + appDb.replaceRuleDao.insert(*it.toTypedArray()) + } + fileToListT(path, "searchHistory.json")?.let { + appDb.searchKeywordDao.insert(*it.toTypedArray()) + } + fileToListT(path, "sourceSub.json")?.let { + appDb.ruleSubDao.insert(*it.toTypedArray()) + } + fileToListT(path, "txtTocRule.json")?.let { + appDb.txtTocRuleDao.insert(*it.toTypedArray()) + } + fileToListT(path, "httpTTS.json")?.let { + appDb.httpTTSDao.insert(*it.toTypedArray()) + } + fileToListT(path, "dictRule.json")?.let { + appDb.dictRuleDao.insert(*it.toTypedArray()) + } + fileToListT(path, "keyboardAssists.json")?.let { + appDb.keyboardAssistsDao.insert(*it.toTypedArray()) + } + fileToListT(path, "readRecord.json")?.let { + it.forEach { readRecord -> + //判断是不是本机记录 + if (readRecord.deviceId != androidId) { + appDb.readRecordDao.insert(readRecord) + } else { + val time = appDb.readRecordDao + .getReadTime(readRecord.deviceId, readRecord.bookName) + if (time == null || time < readRecord.readTime) { + appDb.readRecordDao.insert(readRecord) + } + } + } + } + File(path, "servers.json").takeIf { + it.exists() + }?.runCatching { + var json = readText() + if (!json.isJsonArray()) { + json = aes.decryptStr(json) + } + GSON.fromJsonArray(json).getOrNull()?.let { + appDb.serverDao.insert(*it.toTypedArray()) + } + }?.onFailure { + AppLog.put("恢复服务器配置出错\n${it.localizedMessage}", it) + } + File(path, DirectLinkUpload.ruleFileName).takeIf { + it.exists() + }?.runCatching { + val json = readText() + ACache.get(cacheDir = false).put(DirectLinkUpload.ruleFileName, json) + }?.onFailure { + AppLog.put("恢复直链上传出错\n${it.localizedMessage}", it) + } + //恢复主题配置 + File(path, ThemeConfig.configFileName).takeIf { + it.exists() + }?.runCatching { + FileUtils.delete(ThemeConfig.configFilePath) + copyTo(File(ThemeConfig.configFilePath)) + ThemeConfig.upConfig() + }?.onFailure { + AppLog.put("恢复主题出错\n${it.localizedMessage}", it) + } + File(path, BookCover.configFileName).takeIf { + it.exists() + }?.runCatching { + val json = readText() + BookCover.saveCoverRule(json) + }?.onFailure { + AppLog.put("恢复封面规则出错\n${it.localizedMessage}", it) + } + if (!BackupConfig.ignoreReadConfig) { + //恢复阅读界面配置 + File(path, ReadBookConfig.configFileName).takeIf { + it.exists() + }?.runCatching { + FileUtils.delete(ReadBookConfig.configFilePath) + copyTo(File(ReadBookConfig.configFilePath)) + ReadBookConfig.initConfigs() + }?.onFailure { + AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it) + } + File(path, ReadBookConfig.shareConfigFileName).takeIf { + it.exists() + }?.runCatching { + FileUtils.delete(ReadBookConfig.shareConfigFilePath) + copyTo(File(ReadBookConfig.shareConfigFilePath)) + ReadBookConfig.initShareConfig() + }?.onFailure { + AppLog.put("恢复阅读界面出错\n${it.localizedMessage}", it) + } + } + //AppWebDav.downBgs() + appCtx.getSharedPreferences(path, "config")?.all?.let { map -> + val edit = appCtx.defaultSharedPreferences.edit() + + map.forEach { (key, value) -> + if (BackupConfig.keyIsNotIgnore(key)) { + when (key) { + PreferKey.webDavPassword -> { + kotlin.runCatching { + aes.decryptStr(value.toString()) + }.getOrNull()?.let { + edit.putString(key, it) + } ?: let { + if (appCtx.getPrefString(PreferKey.webDavPassword) + .isNullOrBlank() + ) { + edit.putString(key, value.toString()) + } + } + } + + else -> when (value) { + is Int -> edit.putInt(key, value) + is Boolean -> edit.putBoolean(key, value) + is Long -> edit.putLong(key, value) + is Float -> edit.putFloat(key, value) + is String -> edit.putString(key, value) + } + } + } + } + edit.apply() + } + ReadBookConfig.apply { + comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect) + readStyleSelect = appCtx.getPrefInt(PreferKey.readStyleSelect) + shareLayout = appCtx.getPrefBoolean(PreferKey.shareLayout) + hideStatusBar = appCtx.getPrefBoolean(PreferKey.hideStatusBar) + hideNavigationBar = appCtx.getPrefBoolean(PreferKey.hideNavigationBar) + autoReadSpeed = appCtx.getPrefInt(PreferKey.autoReadSpeed, 46) + } + appCtx.toastOnUi(R.string.restore_success) + withContext(Main) { + delay(100) + if (!BuildConfig.DEBUG) { + LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon)) + } + ThemeConfig.applyDayNight(appCtx) + } + } + + private inline fun fileToListT(path: String, fileName: String): List? { + try { + val file = File(path, fileName) + if (file.exists()) { + LogUtils.d(TAG, "阅读恢复备份 $fileName 文件大小 ${file.length()}") + FileInputStream(file).use { + return GSON.fromJsonArray(it).getOrThrow().also { list -> + LogUtils.d(TAG, "阅读恢复备份 $fileName 列表大小 ${list.size}") + } + } + } else { + LogUtils.d(TAG, "阅读恢复备份 $fileName 文件不存在") + } + } catch (e: Exception) { + AppLog.put("$fileName\n读取解析出错\n${e.localizedMessage}", e) + appCtx.toastOnUi("$fileName\n读取文件出错\n${e.localizedMessage}") + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt b/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt new file mode 100755 index 000000000..c3e5e0e36 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt @@ -0,0 +1,76 @@ +package io.legado.app.help.update + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import io.legado.app.exception.NoStackTraceException +import java.time.Instant + +data class AppReleaseInfo( + val appVariant: AppVariant, + val createdAt: Long, + val note: String, + val name: String, + val downloadUrl: String, + val assetUrl: String +) { + val versionName: String = name.split("_").getOrNull(2)?.dropLast(2) ?: "" +} + +enum class AppVariant { + OFFICIAL, + BETA_RELEASEA, + BETA_RELEASE, + UNKNOWN; + + fun isBeta(): Boolean { + return this == BETA_RELEASE || this == BETA_RELEASEA + } + +} + +@Keep +data class GithubRelease( + val assets: List?, + val body: String, + @SerializedName("prerelease") + val isPreRelease: Boolean, +) { + fun gitReleaseToAppReleaseInfo(): List { + assets ?: throw NoStackTraceException("获取新版本出错") + return assets + .filter { it.isValid } + .map { it.assetToAppReleaseInfo(isPreRelease, body) } + } +} + +@Keep +data class Asset( + @SerializedName("browser_download_url") + val apkUrl: String, + @SerializedName("content_type") + val contentType: String, + @SerializedName("created_at") + val createdAt: String, + @SerializedName("download_count") + val downloadCount: Int, + val id: Int, + val name: String, + val state: String, + val url: String +) { + val isValid: Boolean + get() = (contentType == "application/vnd.android.package-archive") && (state == "uploaded") + + fun assetToAppReleaseInfo(preRelease: Boolean, note: String): AppReleaseInfo { + val instant = Instant.parse(createdAt) + val timestamp: Long = instant.toEpochMilli() + + val appVariant = when { + preRelease && name.contains("releaseA") -> AppVariant.BETA_RELEASEA + preRelease && name.contains("release") -> AppVariant.BETA_RELEASE + else -> AppVariant.OFFICIAL + } + + return AppReleaseInfo(appVariant, timestamp, note, name, apkUrl, url) + } +} diff --git a/app/src/main/java/io/legado/app/help/update/AppUpdate.kt b/app/src/main/java/io/legado/app/help/update/AppUpdate.kt new file mode 100755 index 000000000..5e905aed2 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/update/AppUpdate.kt @@ -0,0 +1,25 @@ +package io.legado.app.help.update + +import io.legado.app.help.coroutine.Coroutine +import kotlinx.coroutines.CoroutineScope + +object AppUpdate { + + val gitHubUpdate: AppUpdateInterface? by lazy { + AppUpdateGitHub + } + + data class UpdateInfo( + val tagName: String, + val updateLog: String, + val downloadUrl: String, + val fileName: String + ) + + interface AppUpdateInterface { + + fun check(scope: CoroutineScope): Coroutine + + } + +} diff --git a/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt b/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt new file mode 100755 index 000000000..63302f3fd --- /dev/null +++ b/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt @@ -0,0 +1,69 @@ +package io.legado.app.help.update + +import androidx.annotation.Keep +import io.legado.app.constant.AppConst +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.newCallResponse +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import kotlinx.coroutines.CoroutineScope + +@Keep +@Suppress("unused") +object AppUpdateGitHub : AppUpdate.AppUpdateInterface { + + private val checkVariant: AppVariant + get() = when (AppConfig.updateToVariant) { + "official_version" -> AppVariant.OFFICIAL + "beta_release_version" -> AppVariant.BETA_RELEASE + "beta_releaseA_version" -> AppVariant.BETA_RELEASEA + else -> AppConst.appInfo.appVariant + } + + private suspend fun getLatestRelease(): List { + val lastReleaseUrl = if (checkVariant.isBeta()) { + "https://api.github.com/repos/gedoor/legado/releases/tags/beta" + } else { + "https://api.github.com/repos/gedoor/legado/releases/latest" + } + val res = okHttpClient.newCallResponse { + url(lastReleaseUrl) + } + if (!res.isSuccessful) { + throw NoStackTraceException("获取新版本出错(${res.code})") + } + val body = res.body.text() + if (body.isBlank()) { + throw NoStackTraceException("获取新版本出错") + } + return GSON.fromJsonObject(body) + .getOrElse { + throw NoStackTraceException("获取新版本出错 " + it.localizedMessage) + } + .gitReleaseToAppReleaseInfo() + .sortedByDescending { it.createdAt } + } + + override fun check( + scope: CoroutineScope, + ): Coroutine { + return Coroutine.async(scope) { + getLatestRelease() + .filter { it.appVariant == checkVariant } + .firstOrNull { it.versionName > AppConst.appInfo.versionName } + ?.let { + return@async AppUpdate.UpdateInfo( + it.versionName, + it.note, + it.downloadUrl, + it.name + ) + } + ?: throw NoStackTraceException("已是最新版本") + }.timeout(10000) + } +} diff --git a/app/src/main/java/io/legado/app/lib/README.md b/app/src/main/java/io/legado/app/lib/README.md new file mode 100755 index 000000000..11a6b3705 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/README.md @@ -0,0 +1,7 @@ +# 放置一些copy过来的库 + +* dialogs 弹出框 +* icu4j 编码识别库 +* permission 权限申请库 +* theme 主题 +* webDav 网络存储 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/aliyun/ALiYun.kt b/app/src/main/java/io/legado/app/lib/aliyun/ALiYun.kt new file mode 100755 index 000000000..8d2bc8bbf --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/aliyun/ALiYun.kt @@ -0,0 +1,10 @@ +package io.legado.app.lib.aliyun + +object ALiYun { + + fun getToken() { + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt b/app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt new file mode 100755 index 000000000..d054da5d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt @@ -0,0 +1,507 @@ +package io.legado.app.lib.cronet + +import androidx.annotation.Keep +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.CookieManager +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.help.http.okHttpClient +import io.legado.app.utils.DebugLog +import io.legado.app.utils.asIOException +import io.legado.app.utils.splitNotBlank +import kotlinx.coroutines.delay +import okhttp3.Call +import okhttp3.Callback +import okhttp3.EventListener +import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.asResponseBody +import okhttp3.internal.http.HTTP_PERM_REDIRECT +import okhttp3.internal.http.HTTP_TEMP_REDIRECT +import okhttp3.internal.http.HttpMethod +import okio.Buffer +import okio.Source +import okio.Timeout +import okio.buffer +import org.chromium.net.CronetException +import org.chromium.net.UrlRequest +import org.chromium.net.UrlResponseInfo +import java.io.IOException +import java.net.ProtocolException +import java.nio.ByteBuffer +import java.util.Locale +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + + +@Keep +abstract class AbsCallBack( + var originalRequest: Request, + val mCall: Call, + var readTimeoutMillis: Int, + private val eventListener: EventListener? = null, + private val responseCallback: Callback? = null +) : UrlRequest.Callback() { + + var mResponse: Response + private var followCount = 0 + private var request: UrlRequest? = null + private var finished = AtomicBoolean(false) + private val canceled = AtomicBoolean(false) + private val callbackResults = ArrayBlockingQueue(2) + private val urlResponseInfoChain = arrayListOf() + private var cancelJob: Coroutine<*>? = null + private var followRedirect = false + private var enableCookieJar = false + private var redirectRequest: Request? = null + + init { + if (readTimeoutMillis == 0) { + readTimeoutMillis = Int.MAX_VALUE + } + if (originalRequest.header(cookieJarHeader) != null) { + enableCookieJar = true + originalRequest = originalRequest.newBuilder() + .removeHeader(cookieJarHeader).build() + } + } + + + @Throws(IOException::class) + abstract fun waitForDone(urlRequest: UrlRequest): Response + + /** + * 当发生错误时,通知子类终止阻塞抛出错误 + * @param error + */ + abstract fun onError(error: IOException) + + /** + * 请求成功后,通知子类结束阻塞,返回response + * @param response + */ + abstract fun onSuccess(response: Response) + + + override fun onRedirectReceived( + request: UrlRequest, + info: UrlResponseInfo, + newLocationUrl: String + ) { + if (followCount > MAX_FOLLOW_COUNT) { + request.cancel() + onError(IOException("Too many redirect")) + return + } + if (mCall.isCanceled()) { + onError(IOException("Cronet Request Canceled")) + request.cancel() + return + } + followCount += 1 + urlResponseInfoChain.add(info) + val client = okHttpClient + if (originalRequest.url.isHttps + && newLocationUrl.startsWith("http://") + && client.followSslRedirects + ) { + followRedirect = true + } else if (!originalRequest.url.isHttps + && newLocationUrl.startsWith("https://") + && client.followSslRedirects + ) { + followRedirect = true + } else if (okHttpClient.followRedirects) { + followRedirect = true + } + + if (!followRedirect) { + onError(IOException("Too many redirect")) + } else { + val response = toResponse(originalRequest, info, urlResponseInfoChain) + if (enableCookieJar) { + CookieManager.saveResponse(response) + } + redirectRequest = buildRedirectRequest(response, originalRequest.method, newLocationUrl) + } + request.cancel() + } + + + override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) { + this.request = request + + val response: Response + try { + response = toResponse(originalRequest, info, urlResponseInfoChain, CronetBodySource()) + } catch (e: IOException) { + request.cancel() + cancelJob?.cancel() + onError(e) + return + } + + if (enableCookieJar) { + CookieManager.saveResponse(response) + } + + mResponse = response + onSuccess(response) + + //打印协议,用于调试 + val msg = "onResponseStarted[${info.negotiatedProtocol}][${info.httpStatusCode}]${info.url}" + DebugLog.i(javaClass.simpleName, msg) + if (eventListener != null) { + eventListener.responseHeadersEnd(mCall, response) + eventListener.responseBodyStart(mCall) + } + try { + responseCallback?.onResponse(mCall, response) + } catch (e: IOException) { + // Pass? + } + } + + + @Throws(IOException::class) + override fun onReadCompleted( + request: UrlRequest, + info: UrlResponseInfo, + byteBuffer: ByteBuffer + ) { + callbackResults.add(CallbackResult(CallbackStep.ON_READ_COMPLETED, byteBuffer)) + } + + + override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { + callbackResults.add(CallbackResult(CallbackStep.ON_SUCCESS)) + cancelJob?.cancel() + eventListener?.responseBodyEnd(mCall, info.receivedByteCount) + //DebugLog.i(javaClass.simpleName, "end[${info.negotiatedProtocol}]${info.url}") + + eventListener?.callEnd(mCall) + } + + + //UrlResponseInfo可能为null + override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) { + callbackResults.add(CallbackResult(CallbackStep.ON_FAILED, null, error)) + cancelJob?.cancel() + DebugLog.e(javaClass.name, error.message.toString()) + onError(error.asIOException()) + eventListener?.callFailed(mCall, error) + responseCallback?.onFailure(mCall, error) + } + + override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) { + if (followRedirect) { + followRedirect = false + if (enableCookieJar) { + val newRequest = CookieManager.loadRequest(redirectRequest!!) + buildRequest(newRequest, this)?.start() + } else { + buildRequest(redirectRequest!!, this)?.start() + } + return + } + canceled.set(true) + callbackResults.add(CallbackResult(CallbackStep.ON_CANCELED)) + cancelJob?.cancel() + //DebugLog.i(javaClass.simpleName, "cancel[${info?.negotiatedProtocol}]${info?.url}") + eventListener?.callEnd(mCall) + onError(IOException("Cronet Request Canceled")) + } + + fun startCheckCancelJob(request: UrlRequest) { + cancelJob = Coroutine.async { + while (!mCall.isCanceled()) { + delay(1000) + } + request.cancel() + } + } + + init { + mResponse = Response.Builder() + .sentRequestAtMillis(System.currentTimeMillis()) + .request(originalRequest) + .protocol(Protocol.HTTP_1_0) + .code(0) + .message("") + .build() + } + + companion object { + const val MAX_FOLLOW_COUNT = 20 + private val encodingsHandledByCronet = setOf("br", "deflate", "gzip", "x-gzip") + + private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol { + val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault()) + return when { + negotiatedProtocol.contains("h3") -> { + Protocol.QUIC + } + + negotiatedProtocol.contains("quic") -> { + Protocol.QUIC + } + + negotiatedProtocol.contains("spdy") -> { + @Suppress("DEPRECATION") + Protocol.SPDY_3 + } + + negotiatedProtocol.contains("h2") -> { + Protocol.HTTP_2 + } + + negotiatedProtocol.contains("1.1") -> { + Protocol.HTTP_1_1 + } + + else -> { + Protocol.HTTP_1_0 + } + } + } + + private fun headersFromResponse( + responseInfo: UrlResponseInfo, + keepEncodingAffectedHeaders: Boolean + ): Headers { + + val headers = responseInfo.allHeadersAsList + return Headers.Builder().apply { + for ((key, value) in headers) { + try { + + if (!keepEncodingAffectedHeaders + && (key.equals("content-encoding", ignoreCase = true) + || key.equals("Content-Length", ignoreCase = true)) + ) { + // Strip all content encoding headers as decoding is done handled by cronet + continue + } + add(key, value) + } catch (e: Exception) { + DebugLog.w(javaClass.name, "Invalid HTTP header/value: $key$value") + // Ignore that header + } + } + + }.build() + + } + + @Throws(IOException::class) + private fun createResponse( + request: Request, + responseInfo: UrlResponseInfo, + bodySource: Source? = null + ): Response.Builder { + val protocol = protocolFromNegotiatedProtocol(responseInfo) + + val contentEncodingHeaders = + responseInfo.allHeaders.getOrDefault("content-encoding", emptyList()) + val contentEncodingItems = contentEncodingHeaders.flatMap { + it.splitNotBlank(",").toList() + } + val keepEncodingAffectedHeaders = contentEncodingItems.isEmpty() + || !encodingsHandledByCronet.containsAll(contentEncodingItems) + + val headers = headersFromResponse(responseInfo, keepEncodingAffectedHeaders) + val contentLength = if (keepEncodingAffectedHeaders) { + responseInfo.allHeaders["Content-Length"]?.lastOrNull() + } else null + val contentType = responseInfo.allHeaders["content-type"]?.lastOrNull() + ?: "text/plain; charset=\"utf-8\"" + + val responseBody = bodySource?.let { + createResponseBody( + request, + responseInfo.httpStatusCode, + contentType, + contentLength, + bodySource + ) + } ?: ResponseBody.EMPTY + + return Response.Builder() + .request(request) + .receivedResponseAtMillis(System.currentTimeMillis()) + .protocol(protocol) + .code(responseInfo.httpStatusCode) + .message(responseInfo.httpStatusText) + .headers(headers) + .body(responseBody) + } + + private fun buildPriorResponse( + request: Request, + redirectResponseInfos: List, + ): Response? { + var priorResponse: Response? = null + if (redirectResponseInfos.isNotEmpty()) { + for (i in redirectResponseInfos.indices) { + val url = redirectResponseInfos[i].url + val redirectedRequest = request.newBuilder().url(url).build() + priorResponse = createResponse(redirectedRequest, redirectResponseInfos[i]) + .priorResponse(priorResponse) + .build() + } + + } + return priorResponse + } + + @Throws(IOException::class) + private fun createResponseBody( + request: Request, + httpStatusCode: Int, + contentType: String?, + contentLengthString: String?, + bodySource: Source + ): ResponseBody { + + // Ignore content-length header for HEAD requests (consistency with OkHttp) + val contentLength: Long = if (request.method == "HEAD") { + 0 + } else { + contentLengthString?.toLongOrNull() ?: -1 + } + + // Check for absence of body in No Content / Reset Content responses (OkHttp consistency) + if ((httpStatusCode == 204 || httpStatusCode == 205) && contentLength > 0) { + throw ProtocolException( + "HTTP $httpStatusCode had non-zero Content-Length: $contentLengthString" + ) + } + return bodySource.buffer() + .asResponseBody(contentType?.toMediaTypeOrNull(), contentLength) + } + + private fun buildRedirectRequest( + userResponse: Response, + method: String, + newLocationUrl: String + ): Request { + // Most redirects don't include a request body. + val requestBuilder = userResponse.request.newBuilder() + if (HttpMethod.permitsRequestBody(method)) { + val responseCode = userResponse.code + val maintainBody = HttpMethod.redirectsWithBody(method) || + responseCode == HTTP_PERM_REDIRECT || + responseCode == HTTP_TEMP_REDIRECT + if (HttpMethod.redirectsToGet(method) + && responseCode != HTTP_PERM_REDIRECT + && responseCode != HTTP_TEMP_REDIRECT + ) { + requestBuilder.method("GET", null) + } else { + val requestBody = if (maintainBody) userResponse.request.body else null + requestBuilder.method(method, requestBody) + } + if (!maintainBody) { + requestBuilder.removeHeader("Transfer-Encoding") + requestBuilder.removeHeader("Content-Length") + requestBuilder.removeHeader("Content-Type") + } + } + + return requestBuilder.url(newLocationUrl).build() + } + + private fun toResponse( + request: Request, + responseInfo: UrlResponseInfo, + redirectResponseInfos: List, + bodySource: Source? = null + ): Response { + val responseBuilder = createResponse(request, responseInfo, bodySource) + val newRequest = request.newBuilder().url(responseInfo.url).build() + return responseBuilder + .request(newRequest) + .priorResponse(buildPriorResponse(request, redirectResponseInfos)) + .build() + } + } + + inner class CronetBodySource : Source { + + private var buffer = ByteBuffer.allocateDirect(32 * 1024) + private var closed = false + private val timeout = readTimeoutMillis.toLong() + + override fun close() { + cancelJob?.cancel() + if (closed) { + return + } + closed = true + if (!finished.get()) { + request?.cancel() + } + } + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + override fun read(sink: Buffer, byteCount: Long): Long { + if (canceled.get()) { + throw IOException("Cronet Request Canceled") + } + + require(byteCount >= 0L) { "byteCount < 0: $byteCount" } + check(!closed) { "closed" } + + if (finished.get()) { + return -1 + } + + if (byteCount < buffer.limit()) { + buffer.limit(byteCount.toInt()) + } + + request?.read(buffer) + + val result = callbackResults.poll(timeout, TimeUnit.MILLISECONDS) + if (result == null) { + request?.cancel() + throw IOException("Cronet request body read timeout after wait $timeout ms") + } + + return when (result.callbackStep) { + CallbackStep.ON_FAILED -> { + finished.set(true) + buffer = null + throw IOException(result.exception) + } + + CallbackStep.ON_SUCCESS -> { + finished.set(true) + buffer = null + -1 + } + + CallbackStep.ON_CANCELED -> { + buffer = null + throw IOException("Request Canceled") + } + + CallbackStep.ON_READ_COMPLETED -> { + result.buffer!!.flip() + val bytesWritten = sink.write(result.buffer) + result.buffer.clear() + bytesWritten.toLong() + } + } + } + + override fun timeout(): Timeout { + return mCall.timeout() + } + + } +} diff --git a/app/src/main/java/io/legado/app/lib/cronet/BodyUploadProvider.kt b/app/src/main/java/io/legado/app/lib/cronet/BodyUploadProvider.kt new file mode 100755 index 000000000..d03ce896e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/BodyUploadProvider.kt @@ -0,0 +1,67 @@ +package io.legado.app.lib.cronet + +import androidx.annotation.Keep +import okhttp3.RequestBody +import okio.Buffer +import org.chromium.net.UploadDataProvider +import org.chromium.net.UploadDataSink +import java.io.IOException +import java.nio.ByteBuffer + +@Keep +class BodyUploadProvider(private val body: RequestBody) : UploadDataProvider(), AutoCloseable { + + private val buffer = Buffer() + + @Volatile + private var filled: Boolean = false + + init { + fillBuffer() + } + + private fun fillBuffer() { + try { + buffer.clear() + filled = true + body.writeTo(buffer) + buffer.flush() + } catch (e: IOException) { + e.printStackTrace() + } + } + + @Throws(IOException::class) + override fun getLength(): Long { + return body.contentLength() + } + + @Throws(IOException::class) + override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) { + if (!filled) { + fillBuffer() + } + check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" } + var read: Int + var bytesRead = 0 + while (bytesRead == 0) { + read = buffer.read(byteBuffer) + bytesRead += read + } + uploadDataSink.onReadSucceeded(false) + } + + @Throws(IOException::class) + override fun rewind(uploadDataSink: UploadDataSink) { + check(body.isOneShot()) { "Okhttp RequestBody is oneShot" } + filled = false + fillBuffer() + uploadDataSink.onRewindSucceeded() + } + + @Throws(IOException::class) + override fun close() { + buffer.close() + super.close() + } +} diff --git a/app/src/main/java/io/legado/app/lib/cronet/CallbackResult.kt b/app/src/main/java/io/legado/app/lib/cronet/CallbackResult.kt new file mode 100755 index 000000000..e2b31125c --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/CallbackResult.kt @@ -0,0 +1,12 @@ +package io.legado.app.lib.cronet + +import org.chromium.net.CronetException + +import java.nio.ByteBuffer + + +data class CallbackResult( + val callbackStep: CallbackStep, + val buffer: ByteBuffer? = null, + val exception: CronetException? = null +) diff --git a/app/src/main/java/io/legado/app/lib/cronet/CallbackStep.kt b/app/src/main/java/io/legado/app/lib/cronet/CallbackStep.kt new file mode 100755 index 000000000..968b0555f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/CallbackStep.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.cronet + +enum class CallbackStep { + ON_READ_COMPLETED, + ON_SUCCESS, + ON_FAILED, + ON_CANCELED +} diff --git a/app/src/main/java/io/legado/app/lib/cronet/CronetCoroutineInterceptor.kt b/app/src/main/java/io/legado/app/lib/cronet/CronetCoroutineInterceptor.kt new file mode 100755 index 000000000..b6674a1ba --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/CronetCoroutineInterceptor.kt @@ -0,0 +1,121 @@ +package io.legado.app.lib.cronet + +import androidx.annotation.Keep +import io.legado.app.utils.printOnDebug +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import okhttp3.Call +import okhttp3.CookieJar +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.internal.http.receiveHeaders +import org.chromium.net.UrlRequest +import org.chromium.net.UrlResponseInfo +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Keep +@Suppress("unused") +class CronetCoroutineInterceptor(private val cookieJar: CookieJar) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + if (chain.call().isCanceled()) { + throw IOException("Canceled") + } + val original: Request = chain.request() + //Cronet未初始化 + return if (!CronetLoader.install() || cronetEngine == null) { + chain.proceed(original) + } else try { + val builder: Request.Builder = original.newBuilder() + //移除Keep-Alive,手动设置会导致400 BadRequest + builder.removeHeader("Keep-Alive") + builder.removeHeader("Accept-Encoding") + if (cookieJar != CookieJar.NO_COOKIES) { + val cookieStr = getCookie(original.url) + //设置Cookie + if (cookieStr.length > 3) { + builder.addHeader("Cookie", cookieStr) + } + } + + val newReq = builder.build() + val timeout = chain.call().timeout().timeoutNanos() / 1000000 + runBlocking() { + if (timeout > 0) { + withTimeout(timeout) { + proceedWithCronet(newReq, chain.call(), chain.readTimeoutMillis()).also { response -> + cookieJar.receiveHeaders(newReq.url, response.headers) + } + } + } else { + proceedWithCronet(newReq, chain.call(), chain.readTimeoutMillis()).also { response -> + cookieJar.receiveHeaders(newReq.url, response.headers) + } + } + } + + } catch (e: Exception) { + //不能抛出错误,抛出错误会导致应用崩溃 + //遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理 + if (!e.message.toString().contains("ERR_CERT_", true) + && !e.message.toString().contains("ERR_SSL_", true) + ) { + e.printOnDebug() + } + chain.proceed(original) + } + + } + + + private suspend fun proceedWithCronet( + request: Request, + call: Call, + readTimeoutMillis: Int + ): Response = + suspendCancellableCoroutine { coroutine -> + + val callBack = object : AbsCallBack(request, call, readTimeoutMillis) { + override fun waitForDone(urlRequest: UrlRequest): Response { + TODO("Not yet implemented") + } + + override fun onError(error: IOException) { + coroutine.resumeWithException(error) + } + + override fun onSuccess(response: Response) { + coroutine.resume(response) + } + + override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) { + super.onCanceled(request, info) + coroutine.cancel() + } + + + } + + val req = buildRequest(request, callBack)?.also { it.start() } + coroutine.invokeOnCancellation { + req?.cancel() + } + + + } + + + /** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */ + private fun getCookie(url: HttpUrl): String = buildString { + val cookies = cookieJar.loadForRequest(url) + cookies.forEachIndexed { index, cookie -> + if (index > 0) append("; ") + append(cookie.name).append('=').append(cookie.value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/cronet/CronetHelper.kt b/app/src/main/java/io/legado/app/lib/cronet/CronetHelper.kt new file mode 100755 index 000000000..786b04198 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/CronetHelper.kt @@ -0,0 +1,121 @@ +@file:Keep +@file:Suppress("DEPRECATION") + +package io.legado.app.lib.cronet + +import androidx.annotation.Keep +import io.legado.app.constant.AppLog +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.help.http.SSLHelper +import io.legado.app.help.http.okHttpClient +import io.legado.app.utils.DebugLog +import io.legado.app.utils.externalCache +import okhttp3.Headers +import okhttp3.MediaType +import okhttp3.Request +import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK +import org.chromium.net.ExperimentalCronetEngine +import org.chromium.net.UploadDataProvider +import org.chromium.net.UrlRequest +import org.chromium.net.X509Util +import org.json.JSONObject +import splitties.init.appCtx + +internal const val BUFFER_SIZE = 32 * 1024 + +val cronetEngine: ExperimentalCronetEngine? by lazy { + CronetLoader.preDownload() + disableCertificateVerify() + val builder = ExperimentalCronetEngine.Builder(appCtx).apply { + if (CronetLoader.install()) { + setLibraryLoader(CronetLoader)//设置自定义so库加载 + } + setStoragePath(appCtx.externalCache.absolutePath)//设置缓存路径 + enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50).toLong())//设置50M的磁盘缓存 + enableQuic(true)//设置支持http/3 + enableHttp2(true) //设置支持http/2 + enablePublicKeyPinningBypassForLocalTrustAnchors(true) + enableBrotli(true)//Brotli压缩 + setExperimentalOptions(options) + } + try { + val engine = builder.build() + DebugLog.d("Cronet Version:", engine.versionString) + return@lazy engine + } catch (e: Throwable) { + AppLog.put("初始化cronetEngine出错", e) + return@lazy null + } +} + +val options by lazy { + val options = JSONObject() + + //设置域名映射规则 + //MAP hostname ip,MAP hostname ip +// val host = JSONObject() +// host.put("host_resolver_rules","") +// options.put("HostResolverRules", host) + + //启用DnsHttpsSvcb更容易迁移到http3 + val dnsSvcb = JSONObject() + dnsSvcb.put("enable", true) + dnsSvcb.put("enable_insecure", true) + dnsSvcb.put("use_alpn", true) + options.put("UseDnsHttpsSvcb", dnsSvcb) + + options.put("AsyncDNS", JSONObject("{'enable':true}")) + + + options.toString() +} + +fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest? { + val url = request.url.toString() + val headers: Headers = request.headers + val requestBody = request.body + return cronetEngine?.newUrlRequestBuilder( + url, + callback, + okHttpClient.dispatcher.executorService + )?.apply { + setHttpMethod(request.method)//设置 + allowDirectExecutor() + headers.forEachIndexed { index, _ -> + if (headers.name(index) == cookieJarHeader) return@forEachIndexed + addHeader(headers.name(index), headers.value(index)) + } + if (requestBody != null) { + val contentType: MediaType? = requestBody.contentType() + if (contentType != null) { + addHeader("Content-Type", contentType.toString()) + } else { + addHeader("Content-Type", "text/plain") + } + val provider: UploadDataProvider = if (requestBody.contentLength() > BUFFER_SIZE) { + LargeBodyUploadProvider(requestBody, okHttpClient.dispatcher.executorService) + } else { + BodyUploadProvider(requestBody) + } + provider.use { + this.setUploadDataProvider(it, okHttpClient.dispatcher.executorService) + } + + } + + }?.build() + +} + +private fun disableCertificateVerify() { + runCatching { + val sDefaultTrustManager = X509Util::class.java.getDeclaredField("sDefaultTrustManager") + sDefaultTrustManager.isAccessible = true + sDefaultTrustManager.set(null, SSLHelper.unsafeTrustManagerExtensions) + } + runCatching { + val sTestTrustManager = X509Util::class.java.getDeclaredField("sTestTrustManager") + sTestTrustManager.isAccessible = true + sTestTrustManager.set(null, SSLHelper.unsafeTrustManagerExtensions) + } +} diff --git a/app/src/main/java/io/legado/app/lib/cronet/CronetInterceptor.kt b/app/src/main/java/io/legado/app/lib/cronet/CronetInterceptor.kt new file mode 100755 index 000000000..4b333d4e6 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/CronetInterceptor.kt @@ -0,0 +1,97 @@ +package io.legado.app.lib.cronet + +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.Keep +import io.legado.app.help.http.CookieManager +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.utils.printOnDebug +import okhttp3.Call +import okhttp3.CookieJar +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +@Keep +@Suppress("unused") +class CronetInterceptor(private val cookieJar: CookieJar) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + if (chain.call().isCanceled()) { + throw IOException("Canceled") + } + val original: Request = chain.request() + //Cronet未初始化 + if (!CronetLoader.install() || cronetEngine == null) { + return chain.proceed(original) + } + val cronetException: Exception + try { + val builder: Request.Builder = original.newBuilder() + //移除Keep-Alive,手动设置会导致400 BadRequest + builder.removeHeader("Keep-Alive") + builder.removeHeader("Accept-Encoding") + + // https://github.com/gedoor/legado/issues/5025#issuecomment-2851156500 + if (!original.isHttps && + original.header("User-Agent")?.startsWith("Mozilla", true) == true + ) { + val referer = original.header("Referer") + if (referer != null && referer.startsWith("https:", true)) { + builder.header("Referer", "http" + referer.substring(5)) + } + } + + var newReq = builder.build() + + if (newReq.header(cookieJarHeader) != null) { + newReq = CookieManager.loadRequest(newReq) + } + + return proceedWithCronet(newReq, chain.call(), chain.readTimeoutMillis())!! + } catch (e: Exception) { + cronetException = e + //不能抛出错误,抛出错误会导致应用崩溃 + //遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理 + if (!e.message.toString().contains("ERR_CERT_", true) + && !e.message.toString().contains("ERR_SSL_", true) + ) { + e.printOnDebug() + } + } + try { + return chain.proceed(original) + } catch (e: Exception) { + e.addSuppressed(cronetException) + throw e + } + } + + @SuppressLint("ObsoleteSdkInt") + @Throws(IOException::class) + private fun proceedWithCronet(request: Request, call: Call, readTimeoutMillis: Int): Response? { + val callBack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + NewCallBack(request, call, readTimeoutMillis) + } else { + OldCallback(request, call, readTimeoutMillis) + } + buildRequest(request, callBack)?.let { + return callBack.waitForDone(it) + } + return null + } + + + /** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */ + private fun getCookie(url: HttpUrl): String = buildString { + val cookies = cookieJar.loadForRequest(url) + cookies.forEachIndexed { index, cookie -> + if (index > 0) append("; ") + append(cookie.name).append('=').append(cookie.value) + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/cronet/CronetLoader.kt b/app/src/main/java/io/legado/app/lib/cronet/CronetLoader.kt new file mode 100755 index 000000000..2ef745305 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/CronetLoader.kt @@ -0,0 +1,375 @@ +package io.legado.app.lib.cronet + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.Build +import android.text.TextUtils +import androidx.annotation.Keep +import io.legado.app.BuildConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.Cronet +import io.legado.app.utils.DebugLog +import io.legado.app.utils.printOnDebug +import org.chromium.net.CronetEngine +import org.json.JSONObject +import splitties.init.appCtx +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.math.BigInteger +import java.net.HttpURLConnection +import java.net.URL +import java.security.MessageDigest +import java.util.Objects + +@Suppress("ConstPropertyName") +@Keep +object CronetLoader : CronetEngine.Builder.LibraryLoader(), Cronet.LoaderInterface { + //https://storage.googleapis.com/chromium-cronet/android/92.0.4515.159/Release/cronet/libs/arm64-v8a/libcronet.92.0.4515.159.so + + private const val soVersion = BuildConfig.Cronet_Version + private const val soName = "libcronet.$soVersion.so" + private val soUrl: String + private val soFile: File + private val downloadFile: File + private var cpuAbi: String? = null + private var md5: String + var download = false + + @Volatile + private var cacheInstall = false + + init { + soUrl = ("https://storage.googleapis.com/chromium-cronet/android/" + + soVersion + "/Release/cronet/libs/" + + getCpuAbi(appCtx) + "/" + soName) + md5 = getMd5(appCtx) + val dir = appCtx.getDir("cronet", Context.MODE_PRIVATE) + soFile = File(dir.toString() + "/" + getCpuAbi(appCtx), soName) + downloadFile = File(appCtx.cacheDir.toString() + "/so_download", soName) + DebugLog.d(javaClass.simpleName, "soName+:$soName") + DebugLog.d(javaClass.simpleName, "destSuccessFile:$soFile") + DebugLog.d(javaClass.simpleName, "tempFile:$downloadFile") + DebugLog.d(javaClass.simpleName, "soUrl:$soUrl") + } + + /** + * 判断Cronet是否安装完成 + */ + override fun install(): Boolean { + synchronized(this) { + if (cacheInstall) { + return true + } + } + + if (md5.length != 32 || !soFile.exists() || md5 != getFileMD5(soFile)) { + cacheInstall = false + return cacheInstall + } + cacheInstall = soFile.exists() + return cacheInstall + } + + + /** + * 预加载Cronet + */ + override fun preDownload() { + Coroutine.async { + //md5 = getUrlMd5(md5Url) + if (soFile.exists() && md5 == getFileMD5(soFile)) { + DebugLog.d(javaClass.simpleName, "So 库已存在") + } else { + download(soUrl, md5, downloadFile, soFile) + } + DebugLog.d(javaClass.simpleName, soName) + } + } + + private fun getMd5(context: Context): String { + val stringBuilder = StringBuilder() + return try { + //获取assets资源管理器 + val assetManager = context.assets + //通过管理器打开文件并读取 + val bf = BufferedReader( + InputStreamReader( + assetManager.open("cronet.json") + ) + ) + var line: String? + while (bf.readLine().also { line = it } != null) { + stringBuilder.append(line) + } + JSONObject(stringBuilder.toString()).optString(getCpuAbi(context), "") + } catch (e: java.lang.Exception) { + return "" + } + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + override fun loadLibrary(libName: String) { + DebugLog.d(javaClass.simpleName, "libName:$libName") + val start = System.currentTimeMillis() + @Suppress("SameParameterValue") + try { + //非cronet的so调用系统方法加载 + if (!libName.contains("cronet")) { + System.loadLibrary(libName) + return + } + //以下逻辑为cronet加载,优先加载本地,否则从远程加载 + //首先调用系统行为进行加载 + System.loadLibrary(libName) + DebugLog.d(javaClass.simpleName, "load from system") + } catch (e: Throwable) { + //如果找不到,则从远程下载 + //删除历史文件 + deleteHistoryFile(Objects.requireNonNull(soFile.parentFile), soFile) + //md5 = getUrlMd5(md5Url) + DebugLog.d(javaClass.simpleName, "soMD5:$md5") + if (md5.length != 32 || soUrl.isEmpty()) { + //如果md5或下载的url为空,则调用系统行为进行加载 + System.loadLibrary(libName) + return + } + if (!soFile.exists() || !soFile.isFile) { + soFile.delete() + download(soUrl, md5, downloadFile, soFile) + //如果文件不存在或不是文件,则调用系统行为进行加载 + System.loadLibrary(libName) + return + } + if (soFile.exists()) { + //如果文件存在,则校验md5值 + val fileMD5 = getFileMD5(soFile) + if (fileMD5 != null && fileMD5.equals(md5, ignoreCase = true)) { + //md5值一样,则加载 + System.load(soFile.absolutePath) + DebugLog.d(javaClass.simpleName, "load from:$soFile") + return + } + //md5不一样则删除 + soFile.delete() + } + //不存在则下载 + download(soUrl, md5, downloadFile, soFile) + //使用系统加载方法 + System.loadLibrary(libName) + } finally { + DebugLog.d(javaClass.simpleName, "time:" + (System.currentTimeMillis() - start)) + } + } + + @SuppressLint("DiscouragedPrivateApi") + private fun getCpuAbi(context: Context): String? { + if (cpuAbi != null) { + return cpuAbi + } + // 5.0以上Application才有primaryCpuAbi字段 + try { + val appInfo = context.applicationInfo + val abiField = ApplicationInfo::class.java.getDeclaredField("primaryCpuAbi") + abiField.isAccessible = true + cpuAbi = abiField.get(appInfo) as String? + } catch (e: Exception) { + e.printOnDebug() + } + if (TextUtils.isEmpty(cpuAbi)) { + cpuAbi = Build.SUPPORTED_ABIS[0] + } + return cpuAbi + } + + + /** + * 删除历史文件 + */ + private fun deleteHistoryFile(dir: File, currentFile: File?) { + val files = dir.listFiles() + @Suppress("SameParameterValue") + if (files != null && files.isNotEmpty()) { + for (f in files) { + if (f.exists() && (currentFile == null || f.absolutePath != currentFile.absolutePath)) { + val delete = f.delete() + DebugLog.d(javaClass.simpleName, "delete file: $f result: $delete") + if (!delete) { + f.deleteOnExit() + } + } + } + } + } + + /** + * 下载文件 + */ + private fun downloadFileIfNotExist(url: String, destFile: File): Boolean { + var inputStream: InputStream? = null + var outputStream: OutputStream? = null + try { + val connection = URL(url).openConnection() as HttpURLConnection + inputStream = connection.inputStream + if (destFile.exists()) { + return true + } + destFile.parentFile!!.mkdirs() + destFile.createNewFile() + outputStream = FileOutputStream(destFile) + val buffer = ByteArray(32768) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + outputStream.flush() + } + return true + } catch (e: Throwable) { + e.printOnDebug() + if (destFile.exists() && !destFile.delete()) { + destFile.deleteOnExit() + } + } finally { + if (inputStream != null) { + try { + inputStream.close() + } catch (e: IOException) { + e.printOnDebug() + } + } + if (outputStream != null) { + try { + outputStream.close() + } catch (e: IOException) { + e.printOnDebug() + } + } + } + return false + } + + /** + * 下载并拷贝文件 + */ + @Suppress("SameParameterValue") + @Synchronized + private fun download( + url: String, + md5: String?, + downloadTempFile: File, + destSuccessFile: File + ) { + if (download) { + return + } + download = true + + Coroutine.async { + val result = downloadFileIfNotExist(url, downloadTempFile) + DebugLog.d(javaClass.simpleName, "download result:$result") + //文件md5再次校验 + val fileMD5 = getFileMD5(downloadTempFile) + if (md5 != null && !md5.equals(fileMD5, ignoreCase = true)) { + val delete = downloadTempFile.delete() + if (!delete) { + downloadTempFile.deleteOnExit() + } + download = false + return@async + } + DebugLog.d(javaClass.simpleName, "download success, copy to $destSuccessFile") + //下载成功拷贝文件 + copyFile(downloadTempFile, destSuccessFile) + cacheInstall = false + val parentFile = downloadTempFile.parentFile + @Suppress("SameParameterValue") + (deleteHistoryFile(parentFile!!, null)) + } + } + + /** + * 拷贝文件 + */ + private fun copyFile(source: File?, dest: File?): Boolean { + if (source == null || !source.exists() || !source.isFile || dest == null) { + return false + } + if (source.absolutePath == dest.absolutePath) { + return true + } + var fileInputStream: FileInputStream? = null + var os: FileOutputStream? = null + val parent = dest.parentFile + if (parent != null && !parent.exists()) { + val mkdirs = parent.mkdirs() + if (!mkdirs) { + parent.mkdirs() + } + } + try { + fileInputStream = FileInputStream(source) + os = FileOutputStream(dest, false) + val buffer = ByteArray(1024 * 512) + var length: Int + while (fileInputStream.read(buffer).also { length = it } > 0) { + os.write(buffer, 0, length) + } + return true + } catch (e: Exception) { + e.printOnDebug() + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close() + } catch (e: Exception) { + e.printOnDebug() + } + } + if (os != null) { + try { + os.close() + } catch (e: Exception) { + e.printOnDebug() + } + } + } + return false + } + + /** + * 获得文件md5 + */ + private fun getFileMD5(file: File): String? { + var fileInputStream: FileInputStream? = null + try { + fileInputStream = FileInputStream(file) + val md5 = MessageDigest.getInstance("MD5") + val buffer = ByteArray(1024) + var numRead: Int + while (fileInputStream.read(buffer).also { numRead = it } > 0) { + md5.update(buffer, 0, numRead) + } + return String.format("%032x", BigInteger(1, md5.digest())).lowercase() + } catch (e: Exception) { + e.printOnDebug() + } catch (e: OutOfMemoryError) { + e.printOnDebug() + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close() + } catch (e: Exception) { + e.printOnDebug() + } + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/cronet/LargeBodyUploadProvider.kt b/app/src/main/java/io/legado/app/lib/cronet/LargeBodyUploadProvider.kt new file mode 100755 index 000000000..b07327f73 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/LargeBodyUploadProvider.kt @@ -0,0 +1,75 @@ +package io.legado.app.lib.cronet + +import androidx.annotation.Keep +import okhttp3.RequestBody +import okio.BufferedSource +import okio.Pipe +import okio.buffer +import org.chromium.net.UploadDataProvider +import org.chromium.net.UploadDataSink +import java.io.IOException +import java.nio.ByteBuffer +import java.util.concurrent.ExecutorService + +/** + * 用于上传大型文件 + * + * @property body + * @property executorService + */ +@Keep +class LargeBodyUploadProvider( + private val body: RequestBody, + private val executorService: ExecutorService +) : UploadDataProvider(), AutoCloseable { + private val pipe = Pipe(BUFFER_SIZE.toLong()) + private var source: BufferedSource = pipe.source.buffer() + + @Volatile + private var filled: Boolean = false + override fun getLength(): Long { + return body.contentLength() + } + + override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) { + if (!filled) { + fillBuffer() + } + check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" } + var read: Int + var bytesRead = 0 + while (bytesRead <= 0) { + read = source.read(byteBuffer) + bytesRead += read + } + uploadDataSink.onReadSucceeded(false) + } + + @Synchronized + private fun fillBuffer() { + executorService.submit { + try { + val writeSink = pipe.sink.buffer() + filled = true + body.writeTo(writeSink) + writeSink.flush() + } catch (e: IOException) { + e.printStackTrace() + } + + } + + } + + override fun rewind(p0: UploadDataSink?) { + check(body.isOneShot()) { "Okhttp RequestBody is OneShot" } + filled = false + fillBuffer() + } + + override fun close() { +// pipe.cancel() +// source.close() + super.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/cronet/NewCallBack.kt b/app/src/main/java/io/legado/app/lib/cronet/NewCallBack.kt new file mode 100755 index 000000000..701c9ea49 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/NewCallBack.kt @@ -0,0 +1,53 @@ +package io.legado.app.lib.cronet + +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.Keep +import androidx.annotation.RequiresApi +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import org.chromium.net.UrlRequest +import java.io.IOException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +@SuppressLint("ObsoleteSdkInt") +@Keep +@RequiresApi(api = Build.VERSION_CODES.N) +class NewCallBack(originalRequest: Request, mCall: Call, readTimeoutMillis: Int) : + AbsCallBack(originalRequest, mCall, readTimeoutMillis) { + + private val responseFuture = CompletableFuture() + + @Throws(IOException::class) + override fun waitForDone(urlRequest: UrlRequest): Response { + urlRequest.start() + startCheckCancelJob(urlRequest) + //DebugLog.i(javaClass.simpleName, "start ${originalRequest.method} ${originalRequest.url}") + return if (mCall.timeout().timeoutNanos() > 0) { + responseFuture.get(mCall.timeout().timeoutNanos(), TimeUnit.NANOSECONDS) + } else { + return responseFuture.get() + } + + } + + /** + * 当发生错误时,通知子类终止阻塞抛出错误 + * @param error + */ + override fun onError(error: IOException) { + responseFuture.completeExceptionally(error) + } + + /** + * 请求成功后,通知子类结束阻塞,返回response + * @param response + */ + override fun onSuccess(response: Response) { + responseFuture.complete(response) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/cronet/OldCallback.kt b/app/src/main/java/io/legado/app/lib/cronet/OldCallback.kt new file mode 100755 index 000000000..faf1fc635 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/cronet/OldCallback.kt @@ -0,0 +1,59 @@ +package io.legado.app.lib.cronet + +import android.os.ConditionVariable +import androidx.annotation.Keep +import okhttp3.Call +import okhttp3.Request +import okhttp3.Response +import org.chromium.net.UrlRequest +import java.io.IOException + +@Keep +class OldCallback(originalRequest: Request, mCall: Call, readTimeoutMillis: Int) : + AbsCallBack(originalRequest, mCall, readTimeoutMillis) { + + private val mResponseCondition = ConditionVariable() + private var mException: IOException? = null + + @Throws(IOException::class) + override fun waitForDone(urlRequest: UrlRequest): Response { + //获取okhttp call的完整请求的超时时间 + val timeOutMs: Long = mCall.timeout().timeoutNanos() / 1000000 + urlRequest.start() + startCheckCancelJob(urlRequest) + if (timeOutMs > 0) { + mResponseCondition.block(timeOutMs) + } else { + mResponseCondition.block() + } + //ConditionVariable 正常open或者超时open后,检查urlRequest是否完成 + if (!urlRequest.isDone) { + urlRequest.cancel() + mException = IOException("Cronet timeout after wait " + timeOutMs + "ms") + } + + if (mException != null) { + throw mException as IOException + } + return mResponse + } + + /** + * 当发生错误时,通知子类终止阻塞抛出错误 + * @param error + */ + override fun onError(error: IOException) { + mException = error + mResponseCondition.open() + } + + /** + * 请求成功后,通知子类结束阻塞,返回response + * @param response + */ + override fun onSuccess(response: Response) { + mResponseCondition.open() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt b/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt new file mode 100755 index 000000000..e79156630 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt @@ -0,0 +1,106 @@ +@file:Suppress("unused") + +package io.legado.app.lib.dialogs + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.view.KeyEvent +import android.view.View +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import io.legado.app.R + +@SuppressLint("SupportAnnotationUsage") +interface AlertBuilder { + val ctx: Context + + fun setTitle(title: CharSequence) + + fun setTitle(titleResource: Int) + + fun setMessage(message: CharSequence) + + fun setMessage(messageResource: Int) + + fun setIcon(icon: Drawable) + + fun setIcon(@DrawableRes iconResource: Int) + + fun setCustomTitle(customTitle: View) + + fun setCustomView(customView: View) + + fun setCancelable(isCancelable: Boolean) + + fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun positiveButton( + @StringRes buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? = null + ) + + fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun negativeButton( + @StringRes buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? = null + ) + + fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null) + fun neutralButton( + @StringRes buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? = null + ) + + fun onCancelled(handler: (dialog: DialogInterface) -> Unit) + + fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) + + fun onDismiss(handler: (dialog: DialogInterface) -> Unit) + + fun items( + items: List, + onItemSelected: (dialog: DialogInterface, index: Int) -> Unit + ) + + fun items( + items: List, + onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit + ) + + fun multiChoiceItems( + items: Array, + checkedItems: BooleanArray, + onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit + ) + + fun singleChoiceItems( + items: Array, + checkedItem: Int = 0, + onClick: ((dialog: DialogInterface, which: Int) -> Unit)? = null + ) + + fun build(): D + fun show(): D + + + fun customTitle(view: () -> View) { + setCustomTitle(view()) + } + + fun customView(view: () -> View) { + setCustomView(view()) + } + + fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + positiveButton(android.R.string.ok, handler) + + fun cancelButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + negativeButton(android.R.string.cancel, handler) + + fun yesButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + positiveButton(R.string.yes, handler) + + fun noButton(handler: ((dialog: DialogInterface) -> Unit)? = null) = + negativeButton(R.string.no, handler) +} diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt b/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt new file mode 100755 index 000000000..0998ce5d5 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/dialogs/AndroidAlertBuilder.kt @@ -0,0 +1,171 @@ +package io.legado.app.lib.dialogs + +import android.content.Context +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.view.KeyEvent +import android.view.View +import androidx.appcompat.app.AlertDialog +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.applyTint + +internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder { + private val builder = AlertDialog.Builder(ctx) + + override fun setTitle(title: CharSequence) { + builder.setTitle(title) + } + + override fun setTitle(titleResource: Int) { + builder.setTitle(titleResource) + } + + override fun setMessage(message: CharSequence) { + builder.setMessage(message) + } + + override fun setMessage(messageResource: Int) { + builder.setMessage(messageResource) + } + + override fun setIcon(icon: Drawable) { + builder.setIcon(icon) + } + + override fun setIcon(iconResource: Int) { + builder.setIcon(iconResource) + } + + override fun setCustomTitle(customTitle: View) { + builder.setCustomTitle(customTitle) + } + + override fun setCustomView(customView: View) { + builder.setView(customView) + } + + override fun setCancelable(isCancelable: Boolean) { + builder.setCancelable(isCancelable) + } + + override fun onCancelled(handler: (DialogInterface) -> Unit) { + builder.setOnCancelListener(handler) + } + + override fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) { + builder.setOnKeyListener(handler) + } + + override fun positiveButton( + buttonText: String, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { + builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun positiveButton( + buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { + builder.setPositiveButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun negativeButton( + buttonText: String, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { + builder.setNegativeButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun negativeButton( + buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { + builder.setNegativeButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun neutralButton( + buttonText: String, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { + builder.setNeutralButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun neutralButton( + buttonTextResource: Int, + onClicked: ((dialog: DialogInterface) -> Unit)? + ) { + builder.setNeutralButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) } + } + + override fun onDismiss(handler: (dialog: DialogInterface) -> Unit) { + builder.setOnDismissListener(handler) + } + + override fun items( + items: List, + onItemSelected: (dialog: DialogInterface, index: Int) -> Unit + ) { + builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> + onItemSelected(dialog, which) + } + } + + override fun items( + items: List, + onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit + ) { + builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which -> + onItemSelected(dialog, items[which], which) + } + } + + override fun multiChoiceItems( + items: Array, + checkedItems: BooleanArray, + onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit + ) { + builder.setMultiChoiceItems(items, checkedItems) { dialog, which, isChecked -> + onClick(dialog, which, isChecked) + } + } + + override fun singleChoiceItems( + items: Array, + checkedItem: Int, + onClick: ((dialog: DialogInterface, which: Int) -> Unit)? + ) { + builder.setSingleChoiceItems(items, checkedItem) { dialog, which -> + onClick?.invoke(dialog, which) + } + } + + override fun build(): AlertDialog { + val dialog = builder.create() + if (AppConfig.isEInkMode) { + dialog.window?.run { + val attr = attributes + attr.dimAmount = 0f + attr.windowAnimations = 0 + attributes = attr + setBackgroundDrawableResource(R.drawable.bg_eink_border_dialog) + } + } + return dialog + } + + override fun show(): AlertDialog { + val dialog = builder.show().applyTint() + if (AppConfig.isEInkMode) { + dialog.window?.run { + val attr = attributes + attr.dimAmount = 0f + attr.windowAnimations = 0 + attributes = attr + setBackgroundDrawableResource(R.drawable.bg_eink_border_dialog) + } + } + return dialog + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt b/app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt new file mode 100755 index 000000000..7690a47c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/dialogs/AndroidDialogs.kt @@ -0,0 +1,186 @@ +@file:Suppress("NOTHING_TO_INLINE", "unused", "DEPRECATION") + +package io.legado.app.lib.dialogs + +import android.app.ProgressDialog +import android.content.Context +import android.content.DialogInterface +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment + +fun Context.alert( + title: CharSequence? = null, + message: CharSequence? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertDialog { + return AndroidAlertBuilder(this).apply { + if (title != null) { + this.setTitle(title) + } + if (message != null) { + this.setMessage(message) + } + if (init != null) init() + }.show() +} + +inline fun Fragment.alert( + title: CharSequence? = null, + message: CharSequence? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = requireActivity().alert(title, message, init) + +fun Context.alert( + titleResource: Int? = null, + messageResource: Int? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertDialog { + return AndroidAlertBuilder(this).apply { + if (titleResource != null) { + this.setTitle(titleResource) + } + if (messageResource != null) { + this.setMessage(messageResource) + } + if (init != null) init() + }.show() +} + +inline fun Fragment.alert( + titleResource: Int? = null, + messageResource: Int? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = requireActivity().alert(titleResource, messageResource, init) + +fun Context.alert(init: AlertBuilder.() -> Unit): AlertDialog = + AndroidAlertBuilder(this).apply { + init() + }.show() + +inline fun Fragment.alert(noinline init: AlertBuilder.() -> Unit) = + requireContext().alert(init) + +inline fun Fragment.progressDialog( + title: Int? = null, + message: Int? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().progressDialog(title, message, init) + +fun Context.progressDialog( + title: Int? = null, + message: Int? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, false, init) + + +inline fun Fragment.indeterminateProgressDialog( + title: Int? = null, + message: Int? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().indeterminateProgressDialog(title, message, init) + +fun Context.indeterminateProgressDialog( + title: Int? = null, + message: Int? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, true, init) + +inline fun Fragment.progressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().progressDialog(title, message, init) + +fun Context.progressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title, message, false, init) + + +inline fun Fragment.indeterminateProgressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + noinline init: (ProgressDialog.() -> Unit)? = null +) = requireActivity().indeterminateProgressDialog(title, message, init) + +fun Context.indeterminateProgressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + init: (ProgressDialog.() -> Unit)? = null +) = progressDialog(title, message, true, init) + + +private fun Context.progressDialog( + title: CharSequence? = null, + message: CharSequence? = null, + indeterminate: Boolean, + init: (ProgressDialog.() -> Unit)? = null +) = ProgressDialog(this).apply { + isIndeterminate = indeterminate + if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) + if (message != null) setMessage(message) + if (title != null) setTitle(title) + if (init != null) init() + show() +} + +typealias AlertBuilderFactory = (Context) -> AlertBuilder + +inline fun Fragment.alert( + noinline factory: AlertBuilderFactory, + title: String? = null, + message: String? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = activity?.alert(factory, title, message, init) + +fun Context.alert( + factory: AlertBuilderFactory, + title: String? = null, + message: String? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertBuilder { + return factory(this).apply { + if (title != null) { + this.setTitle(title) + } + if (message != null) { + this.setMessage(message) + } + if (init != null) init() + } +} + +inline fun Fragment.alert( + noinline factory: AlertBuilderFactory, + titleResource: Int? = null, + messageResource: Int? = null, + noinline init: (AlertBuilder.() -> Unit)? = null +) = requireActivity().alert(factory, titleResource, messageResource, init) + +fun Context.alert( + factory: AlertBuilderFactory, + titleResource: Int? = null, + messageResource: Int? = null, + init: (AlertBuilder.() -> Unit)? = null +): AlertBuilder { + return factory(this).apply { + if (titleResource != null) { + this.setTitle(titleResource) + } + if (messageResource != null) { + this.setMessage(messageResource) + } + if (init != null) init() + } +} + +inline fun Fragment.alert( + noinline factory: AlertBuilderFactory, + noinline init: AlertBuilder.() -> Unit +) = requireActivity().alert(factory, init) + +fun Context.alert( + factory: AlertBuilderFactory, + init: AlertBuilder.() -> Unit +): AlertBuilder = factory(this).apply { init() } diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt b/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt new file mode 100755 index 000000000..4cb5a166f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/dialogs/AndroidSelectors.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("unused") + +package io.legado.app.lib.dialogs + +import android.content.Context +import android.content.DialogInterface + +fun Context.selector( + items: List, + onClick: (DialogInterface, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + items(items, onClick) + show() + } +} + +fun Context.selector( + items: List, + onClick: (DialogInterface, T, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + items(items, onClick) + show() + } +} + +fun Context.selector( + title: CharSequence, + items: List, + onClick: (DialogInterface, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + this.setTitle(title) + items(items, onClick) + show() + } +} + +fun Context.selector( + title: CharSequence, + items: List, + onClick: (DialogInterface, T, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + this.setTitle(title) + items(items, onClick) + show() + } +} + +fun Context.selector( + titleSource: Int, + items: List, + onClick: (DialogInterface, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + this.setTitle(titleSource) + items(items, onClick) + show() + } +} + +fun Context.selector( + titleSource: Int, + items: List, + onClick: (DialogInterface, T, Int) -> Unit +) { + with(AndroidAlertBuilder(this)) { + this.setTitle(titleSource) + items(items, onClick) + show() + } +} diff --git a/app/src/main/java/io/legado/app/lib/dialogs/SelectItem.kt b/app/src/main/java/io/legado/app/lib/dialogs/SelectItem.kt new file mode 100755 index 000000000..71a8f613a --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/dialogs/SelectItem.kt @@ -0,0 +1,13 @@ +package io.legado.app.lib.dialogs + +@Suppress("unused") +data class SelectItem( + val title: String, + val value: T +) { + + override fun toString(): String { + return title + } + +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java new file mode 100755 index 000000000..cfd4d6276 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetDetector.java @@ -0,0 +1,587 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + ****************************************************************************** + Copyright (C) 2005-2016, International Business Machines Corporation and * + others. All Rights Reserved. * + ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +import android.os.ParcelFileDescriptor; +import android.system.OsConstants; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + + +/** + * CharsetDetector provides a facility for detecting the + * charset or encoding of character data in an unknown format. + * The input data can either be from an input stream or an array of bytes. + * The result of the detection operation is a list of possibly matching + * charsets, or, for simple use, you can just ask for a Java Reader that + * will will work over the input data. + *

    + * Character set detection is at best an imprecise operation. The detection + * process will attempt to identify the charset that best matches the characteristics + * of the byte data, but the process is partly statistical in nature, and + * the results can not be guaranteed to always be correct. + *

    + * For best accuracy in charset detection, the input data should be primarily + * in a single language, and a minimum of a few hundred bytes worth of plain text + * in the language are needed. The detection process will attempt to + * ignore html or xml style markup that could otherwise obscure the content. + *

    + * + * @stable ICU 3.4 + */ +@SuppressWarnings({"JavaDoc", "unused", "RedundantSuppression"}) +public class CharsetDetector { + +// Question: Should we have getters corresponding to the setters for input text +// and declared encoding? + +// A thought: If we were to create our own type of Java Reader, we could defer +// figuring out an actual charset for data that starts out with too much English +// only ASCII until the user actually read through to something that didn't look +// like 7 bit English. If nothing else ever appeared, we would never need to +// actually choose the "real" charset. All assuming that the application just +// wants the data, and doesn't care about a char set name. + + /** + * Constructor + * + * @stable ICU 3.4 + */ + public CharsetDetector() { + } + + /** + * Set the declared encoding for charset detection. + * The declared encoding of an input text is an encoding obtained + * from an http header or xml declaration or similar source that + * can be provided as additional information to the charset detector. + * A match between a declared encoding and a possible detected encoding + * will raise the quality of that detected encoding by a small delta, + * and will also appear as a "reason" for the match. + *

    + * A declared encoding that is incompatible with the input data being + * analyzed will not be added to the list of possible encodings. + * + * @param encoding The declared encoding + * @stable ICU 3.4 + */ + public CharsetDetector setDeclaredEncoding(String encoding) { + fDeclaredEncoding = encoding; + return this; + } + + /** + * Set the input text (byte) data whose charset is to be detected. + * + * @param in the input text of unknown encoding + * @return This CharsetDetector + * @stable ICU 3.4 + */ + public CharsetDetector setText(@NonNull byte[] in) { + fRawInput = in; + fRawLength = in.length; + + return this; + } + + public CharsetDetector setText(@NonNull ParcelFileDescriptor pfd) { + fRawInput = new byte[kBufSize]; + try { + android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_SET); + fRawLength = android.system.Os.read(pfd.getFileDescriptor(), fRawInput, 0, fRawInput.length); + android.system.Os.lseek(pfd.getFileDescriptor(), 0, OsConstants.SEEK_SET); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + private static final int kBufSize = 8000; + + /** + * Set the input text (byte) data whose charset is to be detected. + *

    + * The input stream that supplies the character data must have markSupported() + * == true; the charset detection process will read a small amount of data, + * then return the stream to its original position via + * the InputStream.reset() operation. The exact amount that will + * be read depends on the characteristics of the data itself. + * + * @param in the input text of unknown encoding + * @return This CharsetDetector + * @stable ICU 3.4 + */ + + public CharsetDetector setText(@NonNull InputStream in) throws IOException { + fInputStream = in; + fInputStream.mark(kBufSize); + fRawInput = new byte[kBufSize]; // Always make a new buffer because the + // previous one may have come from the caller, + // in which case we can't touch it. + fRawLength = 0; + int remainingLength = kBufSize; + while (remainingLength > 0) { + // read() may give data in smallish chunks, esp. for remote sources. Hence, this loop. + int bytesRead = fInputStream.read(fRawInput, fRawLength, remainingLength); + if (bytesRead <= 0) { + break; + } + fRawLength += bytesRead; + remainingLength -= bytesRead; + } + fInputStream.reset(); + + return this; + } + + + /** + * Return the charset that best matches the supplied input data. + *

    + * Note though, that because the detection + * only looks at the start of the input data, + * there is a possibility that the returned charset will fail to handle + * the full set of input data. + * p> + * aise an exception if + *

      + *
    • no charset appears to match the data.
    • + *
    • no input text has been provided
    • + *
    + * + * @return a CharsetMatch object representing the best matching charset, or + * null if there are no matches. + * @stable ICU 3.4 + */ + @Nullable + public CharsetMatch detect() { +// TODO: A better implementation would be to copy the detect loop from +// detectAll(), and cut it short as soon as a match with a high confidence +// is found. This is something to be done later, after things are otherwise +// working. + CharsetMatch[] matches = detectAll(); + + if (matches == null || matches.length == 0) { + return null; + } + + return matches[0]; + } + + /** + * Return an array of all charsets that appear to be plausible + * matches with the input data. The array is ordered with the + * best quality match first. + *

    + * aise an exception if + *

      + *
    • no charsets appear to match the input data.
    • + *
    • no input text has been provided
    • + *
    + * + * @return An array of CharsetMatch objects representing possibly matching charsets. + * @stable ICU 3.4 + */ + public CharsetMatch[] detectAll() { + ArrayList matches = new ArrayList<>(); + + MungeInput(); // Strip html markup, collect byte stats. + + // Iterate over all possible charsets, remember all that + // give a match quality > 0. + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); + boolean active = (fEnabledRecognizers != null) ? fEnabledRecognizers[i] : rcinfo.isDefaultEnabled; + if (active) { + CharsetMatch m = rcinfo.recognizer.match(this); + if (m != null) { + matches.add(m); + } + } + } + Collections.sort(matches); // CharsetMatch compares on confidence + Collections.reverse(matches); // Put best match first. + CharsetMatch[] resultArray = new CharsetMatch[matches.size()]; + resultArray = matches.toArray(resultArray); + return resultArray; + } + + + /** + * Autodetect the charset of an inputStream, and return a Java Reader + * to access the converted input data. + *

    + * This is a convenience method that is equivalent to + * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getReader(); + *

    + * For the input stream that supplies the character data, markSupported() + * must be true; the charset detection will read a small amount of data, + * then return the stream to its original position via + * the InputStream.reset() operation. The exact amount that will + * be read depends on the characteristics of the data itself. + *

    + * Raise an exception if no charsets appear to match the input data. + * + * @param in The source of the byte data in the unknown charset. + * @param declaredEncoding A declared encoding for the data, if available, + * or null or an empty string if none is available. + * @stable ICU 3.4 + */ + public Reader getReader(InputStream in, String declaredEncoding) { + fDeclaredEncoding = declaredEncoding; + + try { + setText(in); + + CharsetMatch match = detect(); + + if (match == null) { + return null; + } + + return match.getReader(); + } catch (IOException e) { + return null; + } + } + + /** + * Autodetect the charset of an inputStream, and return a String + * containing the converted input data. + *

    + * This is a convenience method that is equivalent to + * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getString(); + *

    + * Raise an exception if no charsets appear to match the input data. + * + * @param in The source of the byte data in the unknown charset. + * @param declaredEncoding A declared encoding for the data, if available, + * or null or an empty string if none is available. + * @stable ICU 3.4 + */ + public String getString(byte[] in, String declaredEncoding) { + fDeclaredEncoding = declaredEncoding; + + try { + setText(in); + + CharsetMatch match = detect(); + + if (match == null) { + return null; + } + + return match.getString(-1); + } catch (IOException e) { + return null; + } + } + + + /** + * Get the names of all charsets supported by CharsetDetector class. + *

    + * Note: Multiple different charset encodings in a same family may use + * a single shared name in this implementation. For example, this method returns + * an array including "ISO-8859-1" (ISO Latin 1), but not including "windows-1252" + * (Windows Latin 1). However, actual detection result could be "windows-1252" + * when the input data matches Latin 1 code points with any points only available + * in "windows-1252". + * + * @return an array of the names of all charsets supported by + * CharsetDetector class. + * @stable ICU 3.4 + */ + public static String[] getAllDetectableCharsets() { + String[] allCharsetNames = new String[ALL_CS_RECOGNIZERS.size()]; + for (int i = 0; i < allCharsetNames.length; i++) { + allCharsetNames[i] = ALL_CS_RECOGNIZERS.get(i).recognizer.getName(); + } + return allCharsetNames; + } + + /** + * Test whether or not input filtering is enabled. + * + * @return true if input text will be filtered. + * @stable ICU 3.4 + * @see #enableInputFilter + */ + public boolean inputFilterEnabled() { + return fStripTags; + } + + /** + * Enable filtering of input text. If filtering is enabled, + * text within angle brackets ("<" and ">") will be removed + * before detection. + * + * @param filter true to enable input text filtering. + * @return The previous setting. + * @stable ICU 3.4 + */ + public boolean enableInputFilter(boolean filter) { + boolean previous = fStripTags; + + fStripTags = filter; + + return previous; + } + + /* + * MungeInput - after getting a set of raw input data to be analyzed, preprocess + * it by removing what appears to be html markup. + */ + private void MungeInput() { + int srci; + int dsti = 0; + byte b; + boolean inMarkup = false; + int openTags = 0; + int badTags = 0; + + // + // html / xml markup stripping. + // quick and dirty, not 100% accurate, but hopefully good enough, statistically. + // discard everything within < brackets > + // Count how many total '<' and illegal (nested) '<' occur, so we can make some + // guess as to whether the input was actually marked up at all. + if (fStripTags) { + for (srci = 0; srci < fRawLength && dsti < fInputBytes.length; srci++) { + b = fRawInput[srci]; + if (b == (byte) '<') { + if (inMarkup) { + badTags++; + } + inMarkup = true; + openTags++; + } + + if (!inMarkup) { + fInputBytes[dsti++] = b; + } + + if (b == (byte) '>') { + inMarkup = false; + } + } + + fInputLen = dsti; + } + + // + // If it looks like this input wasn't marked up, or if it looks like it's + // essentially nothing but markup abandon the markup stripping. + // Detection will have to work on the unstripped input. + // + if (openTags < 5 || openTags / 5 < badTags || + (fInputLen < 100 && fRawLength > 600)) { + int limit = fRawLength; + + if (limit > kBufSize) { + limit = kBufSize; + } + + for (srci = 0; srci < limit; srci++) { + fInputBytes[srci] = fRawInput[srci]; + } + fInputLen = srci; + } + + // + // Tally up the byte occurence statistics. + // These are available for use by the various detectors. + // + Arrays.fill(fByteStats, (short) 0); + for (srci = 0; srci < fInputLen; srci++) { + int val = fInputBytes[srci] & 0x00ff; + fByteStats[val]++; + } + + fC1Bytes = false; + for (int i = 0x80; i <= 0x9F; i += 1) { + if (fByteStats[i] != 0) { + fC1Bytes = true; + break; + } + } + } + + /* + * The following items are accessed by individual CharsetRecongizers during + * the recognition process + * + */ + byte[] fInputBytes = // The text to be checked. Markup will have been + new byte[kBufSize]; // removed if appropriate. + + int fInputLen; // Length of the byte data in fInputBytes. + + short[] fByteStats = // byte frequency statistics for the input text. + new short[256]; // Value is percent, not absolute. + // Value is rounded up, so zero really means zero occurences. + + boolean fC1Bytes = // True if any bytes in the range 0x80 - 0x9F are in the input; + false; + + String fDeclaredEncoding; + + + byte[] fRawInput; // Original, untouched input bytes. + // If user gave us a byte array, this is it. + // If user gave us a stream, it's read to a + // buffer here. + int fRawLength; // Length of data in fRawInput array. + + InputStream fInputStream; // User's input stream, or null if the user + // gave us a byte array. + + // + // Stuff private to CharsetDetector + // + private boolean fStripTags = // If true, setText() will strip tags from input text. + false; + + private boolean[] fEnabledRecognizers; // If not null, active set of charset recognizers had + // been changed from the default. The array index is + // corresponding to ALL_RECOGNIZER. See setDetectableCharset(). + + private static class CSRecognizerInfo { + CharsetRecognizer recognizer; + boolean isDefaultEnabled; + + CSRecognizerInfo(CharsetRecognizer recognizer, boolean isDefaultEnabled) { + this.recognizer = recognizer; + this.isDefaultEnabled = isDefaultEnabled; + } + } + + /* + * List of recognizers for all charsets known to the implementation. + */ + private static final List ALL_CS_RECOGNIZERS; + + static { + List list = new ArrayList<>(); + + list.add(new CSRecognizerInfo(new CharsetRecog_UTF8(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_BE(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_LE(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_BE(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_LE(), true)); + + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_sjis(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022JP(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022CN(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022KR(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_gb_18030(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_jp(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_kr(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_big5(), true)); + + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_1(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_2(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_5_ru(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_6_ar(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_7_el(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_I_he(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_he(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1251(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1256(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_KOI8_R(), true)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_9_tr(), true)); + + // IBM 420/424 recognizers are disabled by default + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_rtl(), false)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_ltr(), false)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_rtl(), false)); + list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_ltr(), false)); + + //noinspection Java9CollectionFactory + ALL_CS_RECOGNIZERS = Collections.unmodifiableList(list); + } + + /** + * Get the names of charsets that can be recognized by this CharsetDetector instance. + * + * @return an array of the names of charsets that can be recognized by this CharsetDetector + * instance. + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public String[] getDetectableCharsets() { + List csnames = new ArrayList<>(ALL_CS_RECOGNIZERS.size()); + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); + boolean active = (fEnabledRecognizers == null) ? rcinfo.isDefaultEnabled : fEnabledRecognizers[i]; + if (active) { + csnames.add(rcinfo.recognizer.getName()); + } + } + return csnames.toArray(new String[0]); + } + + /** + * Enable or disable individual charset encoding. + * A name of charset encoding must be included in the names returned by + * {@link #getAllDetectableCharsets()}. + * + * @param encoding the name of charset encoding. + * @param enabled true to enable, or false to disable the + * charset encoding. + * @return A reference to this CharsetDetector. + * @throws IllegalArgumentException when the name of charset encoding is + * not supported. + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + public CharsetDetector setDetectableCharset(String encoding, boolean enabled) { + int modIdx = -1; + boolean isDefaultVal = false; + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + CSRecognizerInfo csrinfo = ALL_CS_RECOGNIZERS.get(i); + if (csrinfo.recognizer.getName().equals(encoding)) { + modIdx = i; + isDefaultVal = (csrinfo.isDefaultEnabled == enabled); + break; + } + } + if (modIdx < 0) { + // No matching encoding found + throw new IllegalArgumentException("Invalid encoding: " + "\"" + encoding + "\""); + } + + if (fEnabledRecognizers == null && !isDefaultVal) { + // Create an array storing the non default setting + fEnabledRecognizers = new boolean[ALL_CS_RECOGNIZERS.size()]; + + // Initialize the array with default info + for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { + fEnabledRecognizers[i] = ALL_CS_RECOGNIZERS.get(i).isDefaultEnabled; + } + } + + if (fEnabledRecognizers != null) { + fEnabledRecognizers[modIdx] = enabled; + } + + return this; + } +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java new file mode 100755 index 000000000..04bbe6222 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetMatch.java @@ -0,0 +1,241 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + * ****************************************************************************** + * Copyright (C) 2005-2016, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + + +/** + * This class represents a charset that has been identified by a CharsetDetector + * as a possible encoding for a set of input data. From an instance of this + * class, you can ask for a confidence level in the charset identification, + * or for Java Reader or String to access the original byte data in Unicode form. + *

    + * Instances of this class are created only by CharsetDetectors. + *

    + * Note: this class has a natural ordering that is inconsistent with equals. + * The natural ordering is based on the match confidence value. + * + * @stable ICU 3.4 + */ +@SuppressWarnings({"JavaDoc", "unused"}) +public class CharsetMatch implements Comparable { + + + /** + * Create a java.io.Reader for reading the Unicode character data corresponding + * to the original byte data supplied to the Charset detect operation. + *

    + * CAUTION: if the source of the byte data was an InputStream, a Reader + * can be created for only one matching char set using this method. If more + * than one charset needs to be tried, the caller will need to reset + * the InputStream and create InputStreamReaders itself, based on the charset name. + * + * @return the Reader for the Unicode character data. + * @stable ICU 3.4 + */ + public Reader getReader() { + InputStream inputStream = fInputStream; + + if (inputStream == null) { + inputStream = new ByteArrayInputStream(fRawInput, 0, fRawLength); + } + + try { + inputStream.reset(); + return new InputStreamReader(inputStream, getName()); + } catch (IOException e) { + return null; + } + } + + /** + * Create a Java String from Unicode character data corresponding + * to the original byte data supplied to the Charset detect operation. + * + * @return a String created from the converted input data. + * @stable ICU 3.4 + */ + public String getString() throws java.io.IOException { + return getString(-1); + + } + + /** + * Create a Java String from Unicode character data corresponding + * to the original byte data supplied to the Charset detect operation. + * The length of the returned string is limited to the specified size; + * the string will be trunctated to this length if necessary. A limit value of + * zero or less is ignored, and treated as no limit. + * + * @param maxLength The maximium length of the String to be created when the + * source of the data is an input stream, or -1 for + * unlimited length. + * @return a String created from the converted input data. + * @stable ICU 3.4 + */ + public String getString(int maxLength) throws java.io.IOException { + String result; + if (fInputStream != null) { + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[1024]; + Reader reader = getReader(); + int max = maxLength < 0 ? Integer.MAX_VALUE : maxLength; + int bytesRead; + + while ((bytesRead = reader.read(buffer, 0, Math.min(max, 1024))) >= 0) { + sb.append(buffer, 0, bytesRead); + max -= bytesRead; + } + + reader.close(); + + return sb.toString(); + } else { + String name = getName(); + /* + * getName() may return a name with a suffix 'rtl' or 'ltr'. This cannot + * be used to open a charset (e.g. IBM424_rtl). The ending '_rtl' or 'ltr' + * should be stripped off before creating the string. + */ + int startSuffix = !name.contains("_rtl") ? name.indexOf("_ltr") : name.indexOf("_rtl"); + if (startSuffix > 0) { + name = name.substring(0, startSuffix); + } + result = new String(fRawInput, name); + } + return result; + + } + + /** + * Get an indication of the confidence in the charset detected. + * Confidence values range from 0-100, with larger numbers indicating + * a better match of the input data to the characteristics of the + * charset. + * + * @return the confidence in the charset match + * @stable ICU 3.4 + */ + public int getConfidence() { + return fConfidence; + } + + /** + * Get the name of the detected charset. + * The name will be one that can be used with other APIs on the + * platform that accept charset names. It is the "Canonical name" + * as defined by the class java.nio.charset.Charset; for + * charsets that are registered with the IANA charset registry, + * this is the MIME-preferred registerd name. + * + * @return The name of the charset. + * @stable ICU 3.4 + * @see java.nio.charset.Charset + * @see java.io.InputStreamReader + */ + public String getName() { + return fCharsetName; + } + + /** + * Get the ISO code for the language of the detected charset. + * + * @return The ISO code for the language or null if the language cannot be determined. + * @stable ICU 3.4 + */ + public String getLanguage() { + return fLang; + } + + /** + * Compare to other CharsetMatch objects. + * Comparison is based on the match confidence value, which + * allows CharsetDetector.detectAll() to order its results. + * + * @param other the CharsetMatch object to compare against. + * @return a negative integer, zero, or a positive integer as the + * confidence level of this CharsetMatch + * is less than, equal to, or greater than that of + * the argument. + * @throws ClassCastException if the argument is not a CharsetMatch. + * @stable ICU 4.4 + */ + @Override + public int compareTo(CharsetMatch other) { + int compareResult = 0; + if (this.fConfidence > other.fConfidence) { + compareResult = 1; + } else if (this.fConfidence < other.fConfidence) { + compareResult = -1; + } + return compareResult; + } + + /* + * Constructor. Implementation internal + */ + CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf) { + fConfidence = conf; + + // The references to the original application input data must be copied out + // of the charset recognizer to here, in case the application resets the + // recognizer before using this CharsetMatch. + if (det.fInputStream == null) { + // We only want the existing input byte data if it came straight from the user, + // not if is just the head of a stream. + fRawInput = det.fRawInput; + fRawLength = det.fRawLength; + } + fInputStream = det.fInputStream; + fCharsetName = rec.getName(); + fLang = rec.getLanguage(); + } + + /* + * Constructor. Implementation internal + */ + CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf, String csName, String lang) { + fConfidence = conf; + + // The references to the original application input data must be copied out + // of the charset recognizer to here, in case the application resets the + // recognizer before using this CharsetMatch. + if (det.fInputStream == null) { + // We only want the existing input byte data if it came straight from the user, + // not if is just the head of a stream. + fRawInput = det.fRawInput; + fRawLength = det.fRawLength; + } + fInputStream = det.fInputStream; + fCharsetName = csName; + fLang = lang; + } + + + // + // Private Data + // + private final int fConfidence; + private byte[] fRawInput = null; // Original, untouched input bytes. + // If user gave us a byte array, this is it. + private int fRawLength; // Length of data in fRawInput array. + + private final InputStream fInputStream; // User's input stream, or null if the user + // gave us a byte array. + + private final String fCharsetName; // The name of the charset this CharsetMatch + // represents. Filled in by the recognizer. + private final String fLang; // The language, if one was determined by + // the recognizer during the detect operation. +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java new file mode 100755 index 000000000..40cf53ea0 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_2022.java @@ -0,0 +1,170 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + ******************************************************************************* + * Copyright (C) 2005 - 2012, International Business Machines Corporation and * + * others. All Rights Reserved. * + ******************************************************************************* + */ +package io.legado.app.lib.icu4j; + +/** + * class CharsetRecog_2022 part of the ICU charset detection imlementation. + * This is a superclass for the individual detectors for + * each of the detectable members of the ISO 2022 family + * of encodings. + *

    + * The separate classes are nested within this class. + */ +abstract class CharsetRecog_2022 extends CharsetRecognizer { + + + /** + * Matching function shared among the 2022 detectors JP, CN and KR + * Counts up the number of legal an unrecognized escape sequences in + * the sample of text, and computes a score based on the total number & + * the proportion that fit the encoding. + * + * @param text the byte buffer containing text to analyse + * @param textLen the size of the text in the byte. + * @param escapeSequences the byte escape sequences to test for. + * @return match quality, in the range of 0-100. + */ + int match(byte[] text, int textLen, byte[][] escapeSequences) { + int i, j; + int escN; + int hits = 0; + int misses = 0; + int shifts = 0; + int quality; + scanInput: + for (i = 0; i < textLen; i++) { + if (text[i] == 0x1b) { + checkEscapes: + for (escN = 0; escN < escapeSequences.length; escN++) { + byte[] seq = escapeSequences[escN]; + + if ((textLen - i) < seq.length) { + continue; + } + + for (j = 1; j < seq.length; j++) { + if (seq[j] != text[i + j]) { + continue checkEscapes; + } + } + + hits++; + i += seq.length - 1; + continue scanInput; + } + + misses++; + } + + if (text[i] == 0x0e || text[i] == 0x0f) { + // Shift in/out + shifts++; + } + } + + if (hits == 0) { + return 0; + } + + // + // Initial quality is based on relative proportion of recongized vs. + // unrecognized escape sequences. + // All good: quality = 100; + // half or less good: quality = 0; + // linear inbetween. + quality = (100 * hits - 100 * misses) / (hits + misses); + + // Back off quality if there were too few escape sequences seen. + // Include shifts in this computation, so that KR does not get penalized + // for having only a single Escape sequence, but many shifts. + if (hits + shifts < 5) { + quality -= (5 - (hits + shifts)) * 10; + } + + if (quality < 0) { + quality = 0; + } + return quality; + } + + + static class CharsetRecog_2022JP extends CharsetRecog_2022 { + private final byte[][] escapeSequences = { + {0x1b, 0x24, 0x28, 0x43}, // KS X 1001:1992 + {0x1b, 0x24, 0x28, 0x44}, // JIS X 212-1990 + {0x1b, 0x24, 0x40}, // JIS C 6226-1978 + {0x1b, 0x24, 0x41}, // GB 2312-80 + {0x1b, 0x24, 0x42}, // JIS X 208-1983 + {0x1b, 0x26, 0x40}, // JIS X 208 1990, 1997 + {0x1b, 0x28, 0x42}, // ASCII + {0x1b, 0x28, 0x48}, // JIS-Roman + {0x1b, 0x28, 0x49}, // Half-width katakana + {0x1b, 0x28, 0x4a}, // JIS-Roman + {0x1b, 0x2e, 0x41}, // ISO 8859-1 + {0x1b, 0x2e, 0x46} // ISO 8859-7 + }; + + @Override + String getName() { + return "ISO-2022-JP"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_2022KR extends CharsetRecog_2022 { + private final byte[][] escapeSequences = { + {0x1b, 0x24, 0x29, 0x43} + }; + + @Override + String getName() { + return "ISO-2022-KR"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_2022CN extends CharsetRecog_2022 { + private final byte[][] escapeSequences = { + {0x1b, 0x24, 0x29, 0x41}, // GB 2312-80 + {0x1b, 0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1 + {0x1b, 0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2 + {0x1b, 0x24, 0x29, 0x45}, // ISO-IR-165 + {0x1b, 0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3 + {0x1b, 0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4 + {0x1b, 0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5 + {0x1b, 0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6 + {0x1b, 0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7 + {0x1b, 0x4e}, // SS2 + {0x1b, 0x4f}, // SS3 + }; + + @Override + String getName() { + return "ISO-2022-CN"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + +} + diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java new file mode 100755 index 000000000..60ff6d746 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_UTF8.java @@ -0,0 +1,99 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/** + * ****************************************************************************** + * Copyright (C) 2005 - 2014, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +/** + * Charset recognizer for UTF-8 + */ +class CharsetRecog_UTF8 extends CharsetRecognizer { + + @Override + String getName() { + return "UTF-8"; + } + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) + */ + @Override + CharsetMatch match(CharsetDetector det) { + boolean hasBOM = false; + int numValid = 0; + int numInvalid = 0; + byte[] input = det.fRawInput; + int i; + int trailBytes = 0; + int confidence; + + if (det.fRawLength >= 3 && + (input[0] & 0xFF) == 0xef && (input[1] & 0xFF) == 0xbb && (input[2] & 0xFF) == 0xbf) { + hasBOM = true; + } + + // Scan for multi-byte sequences + for (i = 0; i < det.fRawLength; i++) { + int b = input[i]; + if ((b & 0x80) == 0) { + continue; // ASCII + } + + // Hi bit on char found. Figure out how long the sequence should be + if ((b & 0x0e0) == 0x0c0) { + trailBytes = 1; + } else if ((b & 0x0f0) == 0x0e0) { + trailBytes = 2; + } else if ((b & 0x0f8) == 0xf0) { + trailBytes = 3; + } else { + numInvalid++; + continue; + } + + // Verify that we've got the right number of trail bytes in the sequence + for (; ; ) { + i++; + if (i >= det.fRawLength) { + break; + } + b = input[i]; + if ((b & 0xc0) != 0x080) { + numInvalid++; + break; + } + if (--trailBytes == 0) { + numValid++; + break; + } + } + } + + // Cook up some sort of confidence score, based on presense of a BOM + // and the existence of valid and/or invalid multi-byte sequences. + confidence = 0; + if (hasBOM && numInvalid == 0) { + confidence = 100; + } else if (hasBOM && numValid > numInvalid * 10) { + confidence = 80; + } else if (numValid > 3 && numInvalid == 0) { + confidence = 100; + } else if (numValid > 0 && numInvalid == 0) { + confidence = 80; + } else if (numValid == 0 && numInvalid == 0) { + // Plain ASCII. Confidence must be > 10, it's more likely than UTF-16, which + // accepts ASCII with confidence = 10. + // TODO: add plain ASCII as an explicitly detected type. + confidence = 15; + } else if (numValid > numInvalid * 10) { + // Probably corruput utf-8 data. Valid sequences aren't likely by chance. + confidence = 25; + } + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java new file mode 100755 index 000000000..82380e654 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_Unicode.java @@ -0,0 +1,198 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + ******************************************************************************* + * Copyright (C) 1996-2013, International Business Machines Corporation and * + * others. All Rights Reserved. * + ******************************************************************************* + * + */ + +package io.legado.app.lib.icu4j; + +/** + * This class matches UTF-16 and UTF-32, both big- and little-endian. The + * BOM will be used if it is present. + */ +abstract class CharsetRecog_Unicode extends CharsetRecognizer { + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#getName() + */ + @Override + abstract String getName(); + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) + */ + @Override + abstract CharsetMatch match(CharsetDetector det); + + static int codeUnit16FromBytes(byte hi, byte lo) { + return ((hi & 0xff) << 8) | (lo & 0xff); + } + + // UTF-16 confidence calculation. Very simple minded, but better than nothing. + // Any 8 bit non-control characters bump the confidence up. These have a zero high byte, + // and are very likely to be UTF-16, although they could also be part of a UTF-32 code. + // NULs are a contra-indication, they will appear commonly if the actual encoding is UTF-32. + // NULs should be rare in actual text. + static int adjustConfidence(int codeUnit, int confidence) { + if (codeUnit == 0) { + confidence -= 10; + } else if ((codeUnit >= 0x20 && codeUnit <= 0xff) || codeUnit == 0x0a) { + confidence += 10; + } + if (confidence < 0) { + confidence = 0; + } else if (confidence > 100) { + confidence = 100; + } + return confidence; + } + + static class CharsetRecog_UTF_16_BE extends CharsetRecog_Unicode { + @Override + String getName() { + return "UTF-16BE"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + byte[] input = det.fRawInput; + int confidence = 10; + + int bytesToCheck = Math.min(input.length, 30); + for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) { + int codeUnit = codeUnit16FromBytes(input[charIndex], input[charIndex + 1]); + if (charIndex == 0 && codeUnit == 0xFEFF) { + confidence = 100; + break; + } + confidence = adjustConfidence(codeUnit, confidence); + if (confidence == 0 || confidence == 100) { + break; + } + } + if (bytesToCheck < 4 && confidence < 100) { + confidence = 0; + } + if (confidence > 0) { + return new CharsetMatch(det, this, confidence); + } + return null; + } + } + + static class CharsetRecog_UTF_16_LE extends CharsetRecog_Unicode { + @Override + String getName() { + return "UTF-16LE"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + byte[] input = det.fRawInput; + int confidence = 10; + + int bytesToCheck = Math.min(input.length, 30); + for (int charIndex = 0; charIndex < bytesToCheck - 1; charIndex += 2) { + int codeUnit = codeUnit16FromBytes(input[charIndex + 1], input[charIndex]); + if (charIndex == 0 && codeUnit == 0xFEFF) { + confidence = 100; + break; + } + confidence = adjustConfidence(codeUnit, confidence); + if (confidence == 0 || confidence == 100) { + break; + } + } + if (bytesToCheck < 4 && confidence < 100) { + confidence = 0; + } + if (confidence > 0) { + return new CharsetMatch(det, this, confidence); + } + return null; + } + } + + static abstract class CharsetRecog_UTF_32 extends CharsetRecog_Unicode { + abstract int getChar(byte[] input, int index); + + @Override + abstract String getName(); + + @Override + CharsetMatch match(CharsetDetector det) { + byte[] input = det.fRawInput; + int limit = (det.fRawLength / 4) * 4; + int numValid = 0; + int numInvalid = 0; + boolean hasBOM = false; + int confidence = 0; + + if (limit == 0) { + return null; + } + if (getChar(input, 0) == 0x0000FEFF) { + hasBOM = true; + } + + for (int i = 0; i < limit; i += 4) { + int ch = getChar(input, i); + + if (ch < 0 || ch >= 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) { + numInvalid += 1; + } else { + numValid += 1; + } + } + + + // Cook up some sort of confidence score, based on presence of a BOM + // and the existence of valid and/or invalid multi-byte sequences. + if (hasBOM && numInvalid == 0) { + confidence = 100; + } else if (hasBOM && numValid > numInvalid * 10) { + confidence = 80; + } else if (numValid > 3 && numInvalid == 0) { + confidence = 100; + } else if (numValid > 0 && numInvalid == 0) { + confidence = 80; + } else if (numValid > numInvalid * 10) { + // Probably corrupt UTF-32BE data. Valid sequences aren't likely by chance. + confidence = 25; + } + + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_UTF_32_BE extends CharsetRecog_UTF_32 { + @Override + int getChar(byte[] input, int index) { + return (input[index + 0] & 0xFF) << 24 | (input[index + 1] & 0xFF) << 16 | + (input[index + 2] & 0xFF) << 8 | (input[index + 3] & 0xFF); + } + + @Override + String getName() { + return "UTF-32BE"; + } + } + + + static class CharsetRecog_UTF_32_LE extends CharsetRecog_UTF_32 { + @Override + int getChar(byte[] input, int index) { + return (input[index + 3] & 0xFF) << 24 | (input[index + 2] & 0xFF) << 16 | + (input[index + 1] & 0xFF) << 8 | (input[index + 0] & 0xFF); + } + + @Override + String getName() { + return "UTF-32LE"; + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java new file mode 100755 index 000000000..0e211a7ab --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_mbcs.java @@ -0,0 +1,552 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + **************************************************************************** + * Copyright (C) 2005-2012, International Business Machines Corporation and * + * others. All Rights Reserved. * + **************************************************************************** + * + */ +package io.legado.app.lib.icu4j; + +import java.util.Arrays; + +/** + * CharsetRecognizer implemenation for Asian - double or multi-byte - charsets. + * Match is determined mostly by the input data adhering to the + * encoding scheme for the charset, and, optionally, + * frequency-of-occurence of characters. + *

    + * Instances of this class are singletons, one per encoding + * being recognized. They are created in the main + * CharsetDetector class and kept in the global list of available + * encodings to be checked. The specific encoding being recognized + * is determined by subclass. + */ +abstract class CharsetRecog_mbcs extends CharsetRecognizer { + + /** + * Get the IANA name of this charset. + * + * @return the charset name. + */ + @Override + abstract String getName(); + + + /** + * Test the match of this charset with the input text data + * which is obtained via the CharsetDetector object. + * + * @param det The CharsetDetector, which contains the input text + * to be checked for being in this charset. + * @return Two values packed into one int (Damn java, anyhow) + *
    + * bits 0-7: the match confidence, ranging from 0-100 + *
    + * bits 8-15: The match reason, an enum-like value. + */ + int match(CharsetDetector det, int[] commonChars) { + @SuppressWarnings("unused") + int singleByteCharCount = 0; //TODO Do we really need this? + int doubleByteCharCount = 0; + int commonCharCount = 0; + int badCharCount = 0; + int totalCharCount = 0; + int confidence = 0; + iteratedChar iter = new iteratedChar(); + + detectBlock: + { + for (iter.reset(); nextChar(iter, det); ) { + totalCharCount++; + if (iter.error) { + badCharCount++; + } else { + long cv = iter.charValue & 0xFFFFFFFFL; + + if (cv <= 0xff) { + singleByteCharCount++; + } else { + doubleByteCharCount++; + if (commonChars != null) { + // NOTE: This assumes that there are no 4-byte common chars. + if (Arrays.binarySearch(commonChars, (int) cv) >= 0) { + commonCharCount++; + } + } + } + } + if (badCharCount >= 2 && badCharCount * 5 >= doubleByteCharCount) { + // Bail out early if the byte data is not matching the encoding scheme. + break detectBlock; + } + } + + if (doubleByteCharCount <= 10 && badCharCount == 0) { + // Not many multi-byte chars. + if (doubleByteCharCount == 0 && totalCharCount < 10) { + // There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes. + // We don't have enough data to have any confidence. + // Statistical analysis of single byte non-ASCII charcters would probably help here. + confidence = 0; + } else { + // ASCII or ISO file? It's probably not our encoding, + // but is not incompatible with our encoding, so don't give it a zero. + confidence = 10; + } + + break detectBlock; + } + + // + // No match if there are too many characters that don't fit the encoding scheme. + // (should we have zero tolerance for these?) + // + if (doubleByteCharCount < 20 * badCharCount) { + confidence = 0; + break detectBlock; + } + + if (commonChars == null) { + // We have no statistics on frequently occuring characters. + // Assess confidence purely on having a reasonable number of + // multi-byte characters (the more the better + confidence = 30 + doubleByteCharCount - 20 * badCharCount; + if (confidence > 100) { + confidence = 100; + } + } else { + // + // Frequency of occurence statistics exist. + // + double maxVal = Math.log((float) doubleByteCharCount / 4); + double scaleFactor = 90.0 / maxVal; + confidence = (int) (Math.log(commonCharCount + 1) * scaleFactor + 10); + confidence = Math.min(confidence, 100); + } + } // end of detectBlock: + + return confidence; + } + + // "Character" iterated character class. + // Recognizers for specific mbcs encodings make their "characters" available + // by providing a nextChar() function that fills in an instance of iteratedChar + // with the next char from the input. + // The returned characters are not converted to Unicode, but remain as the raw + // bytes (concatenated into an int) from the codepage data. + // + // For Asian charsets, use the raw input rather than the input that has been + // stripped of markup. Detection only considers multi-byte chars, effectively + // stripping markup anyway, and double byte chars do occur in markup too. + // + static class iteratedChar { + int charValue = 0; // 1-4 bytes from the raw input data + int nextIndex = 0; + boolean error = false; + boolean done = false; + + void reset() { + charValue = 0; + nextIndex = 0; + error = false; + done = false; + } + + int nextByte(CharsetDetector det) { + if (nextIndex >= det.fRawLength) { + done = true; + return -1; + } + return det.fRawInput[nextIndex++] & 0x00ff; + } + } + + /** + * Get the next character (however many bytes it is) from the input data + * Subclasses for specific charset encodings must implement this function + * to get characters according to the rules of their encoding scheme. + *

    + * This function is not a method of class iteratedChar only because + * that would require a lot of extra derived classes, which is awkward. + * + * @param it The iteratedChar "struct" into which the returned char is placed. + * @param det The charset detector, which is needed to get at the input byte data + * being iterated over. + * @return True if a character was returned, false at end of input. + */ + abstract boolean nextChar(iteratedChar it, CharsetDetector det); + + + /** + * Shift-JIS charset recognizer. + */ + static class CharsetRecog_sjis extends CharsetRecog_mbcs { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0, + 0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5, + 0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc, + 0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341, + 0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389, + 0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa}; + + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte; + firstByte = it.charValue = it.nextByte(det); + if (firstByte < 0) { + return false; + } + + if (firstByte <= 0x7f || (firstByte > 0xa0 && firstByte <= 0xdf)) { + return true; + } + + int secondByte = it.nextByte(det); + if (secondByte < 0) { + return false; + } + it.charValue = (firstByte << 8) | secondByte; + if (!((secondByte >= 0x40 && secondByte <= 0x7f) || (secondByte >= 0x80 && secondByte <= 0xff))) { + // Illegal second byte value. + it.error = true; + } + return true; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + String getName() { + return "Shift_JIS"; + } + + @Override + public String getLanguage() { + return "ja"; + } + + + } + + + /** + * Big5 charset recognizer. + */ + static class CharsetRecog_big5 extends CharsetRecog_mbcs { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446, + 0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3, + 0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548, + 0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8, + 0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da, + 0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3, + 0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59, + 0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c, + 0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44, + 0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f}; + + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte; + firstByte = it.charValue = it.nextByte(det); + if (firstByte < 0) { + return false; + } + + if (firstByte <= 0x7f || firstByte == 0xff) { + // single byte character. + return true; + } + + int secondByte = it.nextByte(det); + if (secondByte < 0) { + return false; + } + it.charValue = (it.charValue << 8) | secondByte; + + if (secondByte < 0x40 || + secondByte == 0x7f || + secondByte == 0xff) { + it.error = true; + } + return true; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + String getName() { + return "Big5"; + } + + + @Override + public String getLanguage() { + return "zh"; + } + } + + + /** + * EUC charset recognizers. One abstract class that provides the common function + * for getting the next character according to the EUC encoding scheme, + * and nested derived classes for EUC_KR, EUC_JP, EUC_CN. + */ + abstract static class CharsetRecog_euc extends CharsetRecog_mbcs { + + /* + * (non-Javadoc) + * Get the next character value for EUC based encodings. + * Character "value" is simply the raw bytes that make up the character + * packed into an int. + */ + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte; + int secondByte; + int thirdByte; + //int fourthByte = 0; + + buildChar: + { + firstByte = it.charValue = it.nextByte(det); + if (firstByte < 0) { + // Ran off the end of the input data + it.done = true; + break buildChar; + } + if (firstByte <= 0x8d) { + // single byte char + break buildChar; + } + + secondByte = it.nextByte(det); + it.charValue = (it.charValue << 8) | secondByte; + + if (firstByte >= 0xA1 && firstByte <= 0xfe) { + // Two byte Char + if (secondByte < 0xa1) { + it.error = true; + } + break buildChar; + } + if (firstByte == 0x8e) { + // Code Set 2. + // In EUC-JP, total char size is 2 bytes, only one byte of actual char value. + // In EUC-TW, total char size is 4 bytes, three bytes contribute to char value. + // We don't know which we've got. + // Treat it like EUC-JP. If the data really was EUC-TW, the following two + // bytes will look like a well formed 2 byte char. + if (secondByte < 0xa1) { + it.error = true; + } + break buildChar; + } + + if (firstByte == 0x8f) { + // Code set 3. + // Three byte total char size, two bytes of actual char value. + thirdByte = it.nextByte(det); + it.charValue = (it.charValue << 8) | thirdByte; + if (thirdByte < 0xa1) { + it.error = true; + } + } + } + + return (!it.done); + } + + /** + * The charset recognize for EUC-JP. A singleton instance of this class + * is created and kept by the public CharsetDetector class + */ + static class CharsetRecog_euc_jp extends CharsetRecog_euc { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2, + 0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3, + 0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4, + 0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de, + 0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef, + 0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af, + 0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7, + 0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1, + 0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee, + 0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1}; + + @Override + String getName() { + return "EUC-JP"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + public String getLanguage() { + return "ja"; + } + } + + /** + * The charset recognize for EUC-KR. A singleton instance of this class + * is created and kept by the public CharsetDetector class + */ + static class CharsetRecog_euc_kr extends CharsetRecog_euc { + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc, + 0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9, + 0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce, + 0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce, + 0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba, + 0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee, + 0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7, + 0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6, + 0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6, + 0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad}; + + @Override + String getName() { + return "EUC-KR"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + public String getLanguage() { + return "ko"; + } + } + } + + /** + * GB-18030 recognizer. Uses simplified Chinese statistics. + */ + static class CharsetRecog_gb_18030 extends CharsetRecog_mbcs { + + /* + * (non-Javadoc) + * Get the next character value for EUC based encodings. + * Character "value" is simply the raw bytes that make up the character + * packed into an int. + */ + @Override + boolean nextChar(iteratedChar it, CharsetDetector det) { + it.error = false; + int firstByte; + int secondByte; + int thirdByte; + int fourthByte; + + buildChar: + { + firstByte = it.charValue = it.nextByte(det); + + if (firstByte < 0) { + // Ran off the end of the input data + it.done = true; + break buildChar; + } + + if (firstByte <= 0x80) { + // single byte char + break buildChar; + } + + secondByte = it.nextByte(det); + it.charValue = (it.charValue << 8) | secondByte; + + if (firstByte >= 0x81 && firstByte <= 0xFE) { + // Two byte Char + if ((secondByte >= 0x40 && secondByte <= 0x7E) || (secondByte >= 80 && secondByte <= 0xFE)) { + break buildChar; + } + + // Four byte char + if (secondByte >= 0x30 && secondByte <= 0x39) { + thirdByte = it.nextByte(det); + + if (thirdByte >= 0x81 && thirdByte <= 0xFE) { + fourthByte = it.nextByte(det); + + if (fourthByte >= 0x30 && fourthByte <= 0x39) { + it.charValue = (it.charValue << 16) | (thirdByte << 8) | fourthByte; + break buildChar; + } + } + } + + it.error = true; + } + } + + return (!it.done); + } + + static int[] commonChars = + // TODO: This set of data comes from the character frequency- + // of-occurence analysis tool. The data needs to be moved + // into a resource and loaded from there. + {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac, + 0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4, + 0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4, + 0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6, + 0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6, + 0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7, + 0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7, + 0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5, + 0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2, + 0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0}; + + + @Override + String getName() { + return "GB18030"; + } + + @Override + CharsetMatch match(CharsetDetector det) { + int confidence = match(det, commonChars); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + @Override + public String getLanguage() { + return "zh"; + } + } + + +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java new file mode 100755 index 000000000..070b7754b --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecog_sbcs.java @@ -0,0 +1,1188 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/* + **************************************************************************** + * Copyright (C) 2005-2013, International Business Machines Corporation and * + * others. All Rights Reserved. * + ************************************************************************** * + * + */ + +package io.legado.app.lib.icu4j; + +/** + * This class recognizes single-byte encodings. Because the encoding scheme is so + * simple, language statistics are used to do the matching. + */ +abstract class CharsetRecog_sbcs extends CharsetRecognizer { + + /* (non-Javadoc) + * @see com.ibm.icu.text.CharsetRecognizer#getName() + */ + @Override + abstract String getName(); + + static class NGramParser { + // private static final int N_GRAM_SIZE = 3; + private static final int N_GRAM_MASK = 0xFFFFFF; + + protected int byteIndex = 0; + private int ngram = 0; + + private final int[] ngramList; + protected byte[] byteMap; + + private int ngramCount; + private int hitCount; + + protected byte spaceChar; + + public NGramParser(int[] theNgramList, byte[] theByteMap) { + ngramList = theNgramList; + byteMap = theByteMap; + + ngram = 0; + + ngramCount = hitCount = 0; + } + + /* + * Binary search for value in table, which must have exactly 64 entries. + */ + private static int search(int[] table, int value) { + int index = 0; + + if (table[index + 32] <= value) { + index += 32; + } + + if (table[index + 16] <= value) { + index += 16; + } + + if (table[index + 8] <= value) { + index += 8; + } + + if (table[index + 4] <= value) { + index += 4; + } + + if (table[index + 2] <= value) { + index += 2; + } + + if (table[index + 1] <= value) { + index += 1; + } + + if (table[index] > value) { + index -= 1; + } + + if (index < 0 || table[index] != value) { + return -1; + } + + return index; + } + + private void lookup(int thisNgram) { + ngramCount += 1; + + if (search(ngramList, thisNgram) >= 0) { + hitCount += 1; + } + + } + + protected void addByte(int b) { + ngram = ((ngram << 8) + (b & 0xFF)) & N_GRAM_MASK; + lookup(ngram); + } + + private int nextByte(CharsetDetector det) { + if (byteIndex >= det.fInputLen) { + return -1; + } + + return det.fInputBytes[byteIndex++] & 0xFF; + } + + protected void parseCharacters(CharsetDetector det) { + int b; + boolean ignoreSpace = false; + + while ((b = nextByte(det)) >= 0) { + byte mb = byteMap[b]; + + // TODO: 0x20 might not be a space in all character sets... + if (mb != 0) { + if (!(mb == spaceChar && ignoreSpace)) { + addByte(mb); + } + + ignoreSpace = (mb == spaceChar); + } + } + + } + + public int parse(CharsetDetector det) { + return parse(det, (byte) 0x20); + } + + public int parse(CharsetDetector det, byte spaceCh) { + + this.spaceChar = spaceCh; + + parseCharacters(det); + + // TODO: Is this OK? The buffer could have ended in the middle of a word... + addByte(spaceChar); + + double rawPercent = (double) hitCount / (double) ngramCount; + +// if (rawPercent <= 2.0) { +// return 0; +// } + + // TODO - This is a bit of a hack to take care of a case + // were we were getting a confidence of 135... + if (rawPercent > 0.33) { + return 98; + } + + return (int) (rawPercent * 300.0); + } + } + + static class NGramParser_IBM420 extends NGramParser { + private byte alef = 0x00; + + protected static byte[] unshapeMap = { +/* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ +/* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x42, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x47, (byte) 0x49, (byte) 0x4A, (byte) 0x4B, (byte) 0x4C, (byte) 0x4D, (byte) 0x4E, (byte) 0x4F, +/* 5- */ (byte) 0x50, (byte) 0x49, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x56, (byte) 0x58, (byte) 0x58, (byte) 0x5A, (byte) 0x5B, (byte) 0x5C, (byte) 0x5D, (byte) 0x5E, (byte) 0x5F, +/* 6- */ (byte) 0x60, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x63, (byte) 0x65, (byte) 0x65, (byte) 0x67, (byte) 0x67, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, +/* 7- */ (byte) 0x69, (byte) 0x71, (byte) 0x71, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x77, (byte) 0x79, (byte) 0x7A, (byte) 0x7B, (byte) 0x7C, (byte) 0x7D, (byte) 0x7E, (byte) 0x7F, +/* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x80, (byte) 0x8B, (byte) 0x8B, (byte) 0x8D, (byte) 0x8D, (byte) 0x8F, +/* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9E, (byte) 0x9E, +/* A- */ (byte) 0x9E, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x9E, (byte) 0xAB, (byte) 0xAB, (byte) 0xAD, (byte) 0xAD, (byte) 0xAF, +/* B- */ (byte) 0xAF, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xB1, (byte) 0xBB, (byte) 0xBB, (byte) 0xBD, (byte) 0xBD, (byte) 0xBF, +/* C- */ (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xBF, (byte) 0xCC, (byte) 0xBF, (byte) 0xCE, (byte) 0xCF, +/* D- */ (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDA, (byte) 0xDC, (byte) 0xDC, (byte) 0xDC, (byte) 0xDF, +/* E- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, +/* F- */ (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + + public NGramParser_IBM420(int[] theNgramList, byte[] theByteMap) { + super(theNgramList, theByteMap); + } + + private byte isLamAlef(byte b) { + if (b == (byte) 0xb2 || b == (byte) 0xb3) { + return (byte) 0x47; + } else if (b == (byte) 0xb4 || b == (byte) 0xb5) { + return (byte) 0x49; + } else if (b == (byte) 0xb8 || b == (byte) 0xb9) { + return (byte) 0x56; + } else + return (byte) 0x00; + } + + /* + * Arabic shaping needs to be done manually. Cannot call ArabicShaping class + * because CharsetDetector is dealing with bytes not Unicode code points. We could + * convert the bytes to Unicode code points but that would leave us dependent + * on CharsetICU which we try to avoid. IBM420 converter amongst different versions + * of JDK can produce different results and therefore is also avoided. + */ + private int nextByte(CharsetDetector det) { + if (byteIndex >= det.fInputLen || det.fInputBytes[byteIndex] == 0) { + return -1; + } + int next; + + alef = isLamAlef(det.fInputBytes[byteIndex]); + if (alef != (byte) 0x00) + next = 0xB1 & 0xFF; + else + next = unshapeMap[det.fInputBytes[byteIndex] & 0xFF] & 0xFF; + + byteIndex++; + + return next; + } + + @Override + protected void parseCharacters(CharsetDetector det) { + int b; + boolean ignoreSpace = false; + + while ((b = nextByte(det)) >= 0) { + byte mb = byteMap[b]; + + // TODO: 0x20 might not be a space in all character sets... + if (mb != 0) { + if (!(mb == spaceChar && ignoreSpace)) { + addByte(mb); + } + + ignoreSpace = (mb == spaceChar); + } + if (alef != (byte) 0x00) { + mb = byteMap[alef & 0xFF]; + + // TODO: 0x20 might not be a space in all character sets... + if (mb != 0) { + if (!(mb == spaceChar && ignoreSpace)) { + addByte(mb); + } + + ignoreSpace = (mb == spaceChar); + } + + } + } + } + } + + + int match(CharsetDetector det, int[] ngrams, byte[] byteMap) { + return match(det, ngrams, byteMap, (byte) 0x20); + } + + int match(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { + NGramParser parser = new NGramParser(ngrams, byteMap); + return parser.parse(det, spaceChar); + } + + @SuppressWarnings("SameParameterValue") + int matchIBM420(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { + NGramParser_IBM420 parser = new NGramParser_IBM420(ngrams, byteMap); + return parser.parse(det, spaceChar); + } + + static class NGramsPlusLang { + int[] fNGrams; + String fLang; + + NGramsPlusLang(String la, int[] ng) { + fLang = la; + fNGrams = ng; + } + } + + static class CharsetRecog_8859_1 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + + private static final NGramsPlusLang[] ngrams_8859_1 = new NGramsPlusLang[]{ + new NGramsPlusLang( + "da", + new int[]{ + 0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620, + 0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320, + 0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520, + 0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572, + }), + new NGramsPlusLang( + "de", + new int[]{ + 0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F, + 0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220, + 0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465, + 0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572, + }), + new NGramsPlusLang( + "en", + new int[]{ + 0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F, + 0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74, + 0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420, + 0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320, + }), + + new NGramsPlusLang( + "es", + new int[]{ + 0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C, + 0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064, + 0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20, + }), + + new NGramsPlusLang( + "fr", + new int[]{ + 0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E, + 0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20, + 0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420, + 0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220, + }), + + new NGramsPlusLang( + "it", + new int[]{ + 0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073, + 0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220, + 0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20, + 0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F, + }), + + new NGramsPlusLang( + "nl", + new int[]{ + 0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665, + 0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E, + 0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F, + 0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F, + }), + + new NGramsPlusLang( + "no", + new int[]{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474, + 0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65, + 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572, + }), + + new NGramsPlusLang( + "pt", + new int[]{ + 0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, + 0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20, + 0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065, + 0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F, + + }), + + new NGramsPlusLang( + "sv", + new int[]{ + 0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469, + 0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220, + 0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20, + 0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220, + }), + + }; + + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1252" : "ISO-8859-1"; + int bestConfidenceSoFar = -1; + String lang = null; + for (NGramsPlusLang ngl : ngrams_8859_1) { + int confidence = match(det, ngl.fNGrams, byteMap); + if (confidence > bestConfidenceSoFar) { + bestConfidenceSoFar = confidence; + lang = ngl.fLang; + } + } + return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang); + } + + + @Override + public String getName() { + return "ISO-8859-1"; + } + } + + + static class CharsetRecog_8859_2 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0x20, + (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, + (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, + (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, + }; + + private static final NGramsPlusLang[] ngrams_8859_2 = new NGramsPlusLang[]{ + new NGramsPlusLang( + "cs", + new int[]{ + 0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F, + 0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465, + 0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865, + 0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564, + }), + new NGramsPlusLang( + "hu", + new int[]{ + 0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69, + 0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20, + 0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061, + 0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320, + }), + new NGramsPlusLang( + "pl", + new int[]{ + 0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779, + 0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20, + 0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769, + 0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720, + }), + new NGramsPlusLang( + "ro", + new int[]{ + 0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69, + 0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070, + 0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72, + 0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20, + }) + }; + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1250" : "ISO-8859-2"; + int bestConfidenceSoFar = -1; + String lang = null; + for (NGramsPlusLang ngl : ngrams_8859_2) { + int confidence = match(det, ngl.fNGrams, byteMap); + if (confidence > bestConfidenceSoFar) { + bestConfidenceSoFar = confidence; + lang = ngl.fLang; + } + } + return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, this, bestConfidenceSoFar, name, lang); + } + + @Override + public String getName() { + return "ISO-8859-2"; + } + + } + + + abstract static class CharsetRecog_8859_5 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, + }; + + @Override + public String getName() { + return "ISO-8859-5"; + } + } + + static class CharsetRecog_8859_5_ru extends CharsetRecog_8859_5 { + private static final int[] ngrams = { + 0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE, + 0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD, + 0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2, + 0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520, + }; + + @Override + public String getLanguage() { + return "ru"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_8859_6 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + }; + + @Override + public String getName() { + return "ISO-8859-6"; + } + } + + static class CharsetRecog_8859_6_ar extends CharsetRecog_8859_6 { + private static final int[] ngrams = { + 0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8, + 0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1, + 0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20, + 0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620, + }; + + @Override + public String getLanguage() { + return "ar"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_8859_7 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xA1, (byte) 0xA2, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xDC, (byte) 0x20, + (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0x20, (byte) 0xFC, (byte) 0x20, (byte) 0xFD, (byte) 0xFE, + (byte) 0xC0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0x20, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, + }; + + @Override + public String getName() { + return "ISO-8859-7"; + } + } + + static class CharsetRecog_8859_7_el extends CharsetRecog_8859_7 { + private static final int[] ngrams = { + 0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7, + 0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120, + 0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5, + 0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20, + }; + + @Override + public String getLanguage() { + return "el"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1253" : "ISO-8859-7"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "el"); + } + } + + abstract static class CharsetRecog_8859_8 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + }; + + @Override + public String getName() { + return "ISO-8859-8"; + } + } + + static class CharsetRecog_8859_8_I_he extends CharsetRecog_8859_8 { + private static final int[] ngrams = { + 0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0, + 0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4, + 0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE, + 0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9, + }; + + @Override + public String getName() { + return "ISO-8859-8-I"; + } + + @Override + public String getLanguage() { + return "he"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8-I"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "he"); + } + } + + static class CharsetRecog_8859_8_he extends CharsetRecog_8859_8 { + private static final int[] ngrams = { + 0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0, + 0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC, + 0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920, + 0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9, + }; + + @Override + public String getLanguage() { + return "he"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "he"); + + } + } + + abstract static class CharsetRecog_8859_9 extends CharsetRecog_sbcs { + protected static byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x69, (byte) 0xFE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + @Override + public String getName() { + return "ISO-8859-9"; + } + } + + static class CharsetRecog_8859_9_tr extends CharsetRecog_8859_9 { + private static final int[] ngrams = { + 0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961, + 0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062, + 0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062, + 0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD, + }; + + @Override + public String getLanguage() { + return "tr"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + String name = det.fC1Bytes ? "windows-1254" : "ISO-8859-9"; + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence, name, "tr"); + } + } + + static class CharsetRecog_windows_1251 extends CharsetRecog_sbcs { + private static final int[] ngrams = { + 0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE, + 0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED, + 0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2, + 0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520, + }; + + private static final byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x90, (byte) 0x83, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, + (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, + (byte) 0x20, (byte) 0xA2, (byte) 0xA2, (byte) 0xBC, (byte) 0x20, (byte) 0xB4, (byte) 0x20, (byte) 0x20, + (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBF, + (byte) 0x20, (byte) 0x20, (byte) 0xB3, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0xBC, (byte) 0xBE, (byte) 0xBE, (byte) 0xBF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, + }; + + @Override + public String getName() { + return "windows-1251"; + } + + @Override + public String getLanguage() { + return "ru"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_windows_1256 extends CharsetRecog_sbcs { + private static final int[] ngrams = { + 0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8, + 0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD, + 0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20, + 0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420, + }; + + private static final byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x81, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x88, (byte) 0x20, (byte) 0x8A, (byte) 0x20, (byte) 0x9C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, + (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x98, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x20, (byte) 0x20, (byte) 0x9F, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0x20, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF4, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0xF9, (byte) 0x20, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0x20, (byte) 0xFF, + }; + + @Override + public String getName() { + return "windows-1256"; + } + + @Override + public String getLanguage() { + return "ar"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_KOI8_R extends CharsetRecog_sbcs { + private static final int[] ngrams = { + 0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1, + 0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE, + 0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1, + 0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF, + }; + + private static final byte[] byteMap = { + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, + (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, + (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, + (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, + (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, + }; + + @Override + public String getName() { + return "KOI8-R"; + } + + @Override + public String getLanguage() { + return "ru"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_IBM424_he extends CharsetRecog_sbcs { + protected static byte[] byteMap = { +/* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ +/* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 4- */ (byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 7- */ (byte) 0x40, (byte) 0x71, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x00, (byte) 0x40, (byte) 0x40, +/* 8- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 9- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* B- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, + }; + + @Override + public String getLanguage() { + return "he"; + } + } + + static class CharsetRecog_IBM424_he_rtl extends CharsetRecog_IBM424_he { + @Override + public String getName() { + return "IBM424_rtl"; + } + + private static final int[] ngrams = { + 0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641, + 0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045, + 0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056, + 0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069, + }; + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + static class CharsetRecog_IBM424_he_ltr extends CharsetRecog_IBM424_he { + @Override + public String getName() { + return "IBM424_ltr"; + } + + private static final int[] ngrams = { + 0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141, + 0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054, + 0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940, + 0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651 + + }; + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = match(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + } + + abstract static class CharsetRecog_IBM420_ar extends CharsetRecog_sbcs { + + protected static byte[] byteMap = { +/* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ +/* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x40, (byte) 0x40, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 7- */ (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, +/* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x8B, (byte) 0x8C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, +/* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9B, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, +/* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF, +/* B- */ (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x40, (byte) 0x40, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0xBD, (byte) 0xBE, (byte) 0xBF, +/* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0xCB, (byte) 0x40, (byte) 0xCD, (byte) 0x40, (byte) 0xCF, +/* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, +/* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xEA, (byte) 0xEB, (byte) 0x40, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, +/* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x40, + }; + + + @Override + public String getLanguage() { + return "ar"; + } + + } + + static class CharsetRecog_IBM420_ar_rtl extends CharsetRecog_IBM420_ar { + private static final int[] ngrams = { + 0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158, + 0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB, + 0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40, + 0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40, + }; + + @Override + public String getName() { + return "IBM420_rtl"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + } + + static class CharsetRecog_IBM420_ar_ltr extends CharsetRecog_IBM420_ar { + private static final int[] ngrams = { + 0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF, + 0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD, + 0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156, + 0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156 + }; + + @Override + public String getName() { + return "IBM420_ltr"; + } + + @Override + public CharsetMatch match(CharsetDetector det) { + int confidence = matchIBM420(det, ngrams, byteMap, (byte) 0x40); + return confidence == 0 ? null : new CharsetMatch(det, this, confidence); + } + + } +} diff --git a/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java new file mode 100755 index 000000000..1adabd330 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java @@ -0,0 +1,53 @@ +// © 2016 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html +/** + * ****************************************************************************** + * Copyright (C) 2005-2012, International Business Machines Corporation and * + * others. All Rights Reserved. * + * ****************************************************************************** + */ +package io.legado.app.lib.icu4j; + +/** + * Abstract class for recognizing a single charset. + * Part of the implementation of ICU's CharsetDetector. + *

    + * Each specific charset that can be recognized will have an instance + * of some subclass of this class. All interaction between the overall + * CharsetDetector and the stuff specific to an individual charset happens + * via the interface provided here. + *

    0 表示短偏移 (Offset16),1 表示长偏移 (Offset32)。 + */ + public short indexToLocFormat; + /** + * int16 + */ + public short glyphDataFormat; + } + + /** + * Maximum Profile + */ + private static class MaxpLayout { + /** + * uint32 高16位表示整数,低16位表示小数 + */ + public int version; + /** + * uint16 字体中的字形数量 + */ + public int numGlyphs; + /** + * uint16 非复合字形中包含的最大点数。点是构成字形轮廓的基本单位。 + */ + public int maxPoints; + /** + * uint16 非复合字形中包含的最大轮廓数。轮廓是由一系列点连接形成的封闭曲线。 + */ + public int maxContours; + /** + * uint16 复合字形中包含的最大点数。复合字形是由多个简单字形组合而成的。 + */ + public int maxCompositePoints; + /** + * uint16 复合字形中包含的最大轮廓数。 + */ + public int maxCompositeContours; + /** + * uint16 + */ + public int maxZones; + /** + * uint16 + */ + public int maxTwilightPoints; + /** + * uint16 + */ + public int maxStorage; + /** + * uint16 + */ + public int maxFunctionDefs; + /** + * uint16 + */ + public int maxInstructionDefs; + /** + * uint16 + */ + public int maxStackElements; + /** + * uint16 + */ + public int maxSizeOfInstructions; + /** + * uint16 任何复合字形在“顶层”引用的最大组件数。 + */ + public int maxComponentElements; + /** + * uint16 递归的最大层数;简单组件为1。 + */ + public int maxComponentDepth; + } + + /** + * 字符到字形索引映射表 + */ + private static class CmapLayout { + /** + * uint16 + */ + public int version; + /** + * uint16 后面的编码表的数量 + */ + public int numTables; + public LinkedList records = new LinkedList<>(); + public HashMap tables = new HashMap<>(); + } + + /** + * Encoding records and encodings + */ + private static class CmapRecord { + /** + * uint16 Platform ID. + *

    0、Unicode + *

    1、Macintosh + *

    2、ISO + *

    3、Windows + *

    4、Custom + */ + public int platformID; + /** + * uint16 Platform-specific encoding ID. + *

    platform ID = 3 + *

    0、Symbol + *

    1、Unicode BMP + *

    2、ShiftJIS + *

    3、PRC + *

    4、Big5 + *

    5、Wansung + *

    6、Johab + *

    7、Reserved + *

    8、Reserved + *

    9、Reserved + *

    10、Unicode full repertoire + */ + public int encodingID; + /** + * uint32 从 cmap 表开头到子表的字节偏移量 + */ + public int offset; + } + + private static class CmapFormat { + /** + * uint16 + *

    cmapFormat 子表的格式类型 + */ + public int format; + /** + * uint16 + *

    这个 Format 表的长度(以字节为单位) + */ + public int length; + /** + * uint16 + *

    仅 platformID=1 时有效 + */ + public int language; + /** + * uint16[256] + *

    仅 Format=2 + *

    将高字节映射到 subHeaders 的数组:值为 subHeader 索引x8 + */ + public int[] subHeaderKeys; + /** + * uint16[] + *

    仅 Format=2 + *

    subHeader 子标头的可变长度数组 + *

    其结构为 uint16[][4]{ {uint16,uint16,int16,uint16}, ... } + */ + public int[] subHeaders; + /** + * uint16 segCount x2 + *

    仅 Format=4 + *

    seg段计数乘以 2。这是因为每个段用两个字节表示,所以这个值是实际段数的两倍。 + */ + public int segCountX2; + /** + * uint16 + *

    仅 Format=4 + *

    小于或等于段数的最大二次幂,再乘以 2。这是为二分查找优化搜索过程。 + */ + public int searchRange; + /** + * uint16 + *

    仅 Format=4 + *

    等于 log2(searchRange/2),这是最大二次幂的对数。 + */ + public int entrySelector; + /** + * uint16 + *

    仅 Format=4 + *

    segCount * 2 - searchRange 用于调整搜索范围的偏移。 + */ + public int rangeShift; + /** + * uint16[segCount] + *

    仅 Format=4 + *

    每个段的结束字符码,最后一个是 0xFFFF,表示 Unicode 范围的结束。 + */ + public int[] endCode; + /** + * uint16 + *

    仅 Format=4 + *

    固定设置为 0,用于填充保留位以保持数据对齐。 + */ + public int reservedPad; + /** + * uint16[segCount] + *

    仅 Format=4 + *

    每个段的起始字符码。 + */ + public int[] startCode; + /** + * int16[segCount] + *

    仅 Format=4 + *

    用于计算字形索引的偏移值。该值被加到从 startCode 到 endCode 的所有字符码上,得到相应的字形索引。 + */ + public int[] idDelta; + /** + * uint16[segCount] + *

    仅 Format=4 + *

    偏移到 glyphIdArray 中的起始位置,如果没有额外的字形索引映射,则为 0。 + */ + public int[] idRangeOffsets; + /** + * uint16 + *

    仅 Format=6 + *

    子范围的第一个字符代码。这是连续字符代码范围的起始点。 + */ + public int firstCode; + /** + * uint16 + *

    仅 Format=6 + *

    子范围中字符代码的数量。这表示从 firstCode 开始,连续多少个字符代码被包含 + */ + public int entryCount; + /** + * 字形索引数组 + *

    Format=0 为 bye[256]数组 + *

    Format>0 为 uint16[] 数组 + *

    Format>12 为 uint32[] 数组 + *

    @url Microsoft cmap文档 + */ + public int[] glyphIdArray; + } + + /** + * 字形轮廓数据表 + */ + private static class GlyfLayout { + /** + * int16 非负值为简单字形的轮廓数,负值表示为复合字形 + */ + public short numberOfContours; + /** + * int16 Minimum x for coordinate data. + */ + public short xMin; + /** + * int16 Minimum y for coordinate data. + */ + public short yMin; + /** + * int16 Maximum x for coordinate data. + */ + public short xMax; + /** + * int16 Maximum y for coordinate data. + */ + public short yMax; + /** + * 简单字形数据 + */ + public GlyphTableBySimple glyphSimple; + /** + * 复合字形数据 + */ + public LinkedList glyphComponent; + } + + /** + * 简单字形数据表 + */ + private static class GlyphTableBySimple { + /** + * uint16[numberOfContours] + */ + int[] endPtsOfContours; + /** + * uint16 + */ + int instructionLength; + /** + * uint8[instructionLength] + */ + int[] instructions; + /** + * uint8[variable] + *

    bit0: 该点位于曲线上 + *

    bit1: < 1:xCoordinate为uint8 > + *

    bit2: < 1:yCoordinate为uint8 > + *

    bit3: < 1:下一个uint8为此条目之后插入的附加逻辑标志条目的数量 > + *

    bit4: < bit1=1时表示符号[1.正,0.负]; bit1=0时[1.x坐标重复一次,0.x坐标读为int16] > + *

    bit5: < bit2=1时表示符号[1.正,0.负]; bit2=0时[1.y坐标重复一次,0.y坐标读为int16] > + *

    bit6: 字形描述中的轮廓可能会重叠 + *

    bit7: 保留位,无意义 + */ + int[] flags; + /** + * uint8[] when(flags&0x02==0x02) + * int16[] when(flags&0x12==0x00) + */ + int[] xCoordinates; + /** + * uint8[] when(flags&0x04==0x02) + * int16[] when(flags&0x24==0x00) + */ + int[] yCoordinates; + } + + /** + * 复合字形数据表 + */ + private static class GlyphTableComponent { + /** + * uint16 + *

    bit0: < 1:argument是16bit,0:argument是8bit > + *

    bit1: < 1:argument是有符号值,0:argument是无符号值 > + *

    bit3: 该组件有一个缩放比例,否则比例为1.0 + *

    bit5: 表示在此字形之后还有字形 + */ + int flags; + /** + * uint16 + */ + int glyphIndex; + /** + * x-offset + *

    uint8 when flags&0x03==0 + *

    int8 when flags&0x03==1 + *

    uint16 when flags&0x03==2 + *

    int16 when flags&0x03==3 + */ + int argument1; + /** + * y-offset + *

    uint8 when flags&0x03==0 + *

    int8 when flags&0x03==1 + *

    uint16 when flags&0x03==2 + *

    int16 when flags&0x03==3 + */ + int argument2; + /** + * uint16 + *

    值类型为 F2DOT14 的组件缩放X比例值 + */ + float xScale; + /** + * uint16 + *

    值类型为 F2DOT14 的2x2变换矩阵01值 + */ + float scale01; + /** + * uint16 + *

    值类型为 F2DOT14 的2x2变换矩阵10值 + */ + float scale10; + /** + * uint16 + *

    值类型为 F2DOT14 的组件缩放Y比例值 + */ + float yScale; + } + + private static class BufferReader { + private final ByteBuffer byteBuffer; + + public BufferReader(byte[] buffer, int index) { + this.byteBuffer = ByteBuffer.wrap(buffer); + this.byteBuffer.order(ByteOrder.BIG_ENDIAN); // 设置为大端模式 + this.byteBuffer.position(index); // 设置起始索引 + } + + public void position(int index) { + byteBuffer.position(index); // 设置起始索引 + } + + public int position() { + return byteBuffer.position(); + } + + public long ReadUInt64() { + return byteBuffer.getLong(); + } + + public int ReadUInt32() { + return byteBuffer.getInt(); + } + + public int ReadInt32() { + return byteBuffer.getInt(); + } + + public int ReadUInt16() { + return byteBuffer.getShort() & 0xFFFF; + } + + public short ReadInt16() { + return byteBuffer.getShort(); + } + + public short ReadUInt8() { + return (short) (byteBuffer.get() & 0xFF); + } + + public byte ReadInt8() { + return byteBuffer.get(); + } + + public byte[] ReadByteArray(int len) { + assert len >= 0; + byte[] result = new byte[len]; + byteBuffer.get(result); + return result; + } + + public int[] ReadUInt8Array(int len) { + assert len >= 0; + var result = new int[len]; + for (int i = 0; i < len; ++i) result[i] = byteBuffer.get() & 0xFF; + return result; + } + + public int[] ReadInt16Array(int len) { + assert len >= 0; + var result = new int[len]; + for (int i = 0; i < len; ++i) result[i] = byteBuffer.getShort(); + return result; + } + + public int[] ReadUInt16Array(int len) { + assert len >= 0; + var result = new int[len]; + for (int i = 0; i < len; ++i) result[i] = byteBuffer.getShort() & 0xFFFF; + return result; + } + + public int[] ReadInt32Array(int len) { + assert len >= 0; + var result = new int[len]; + for (int i = 0; i < len; ++i) result[i] = byteBuffer.getInt(); + return result; + } + } + + private final Header fileHeader = new Header(); + private final HashMap directorys = new HashMap<>(); + private final NameLayout name = new NameLayout(); + private final HeadLayout head = new HeadLayout(); + private final MaxpLayout maxp = new MaxpLayout(); + private final CmapLayout Cmap = new CmapLayout(); + private final int[][] pps = new int[][]{{3, 10}, {0, 4}, {3, 1}, {1, 0}, {0, 3}, {0, 1}}; + + private void readNameTable(byte[] buffer) { + var dataTable = directorys.get("name"); + assert dataTable != null; + var reader = new BufferReader(buffer, dataTable.offset); + name.format = reader.ReadUInt16(); + name.count = reader.ReadUInt16(); + name.stringOffset = reader.ReadUInt16(); + for (int i = 0; i < name.count; ++i) { + NameRecord record = new NameRecord(); + record.platformID = reader.ReadUInt16(); + record.encodingID = reader.ReadUInt16(); + record.languageID = reader.ReadUInt16(); + record.nameID = reader.ReadUInt16(); + record.length = reader.ReadUInt16(); + record.offset = reader.ReadUInt16(); + name.records.add(record); + } + } + + private void readHeadTable(byte[] buffer) { + var dataTable = directorys.get("head"); + assert dataTable != null; + var reader = new BufferReader(buffer, dataTable.offset); + head.majorVersion = reader.ReadUInt16(); + head.minorVersion = reader.ReadUInt16(); + head.fontRevision = reader.ReadUInt32(); + head.checkSumAdjustment = reader.ReadUInt32(); + head.magicNumber = reader.ReadUInt32(); + head.flags = reader.ReadUInt16(); + head.unitsPerEm = reader.ReadUInt16(); + head.created = reader.ReadUInt64(); + head.modified = reader.ReadUInt64(); + head.xMin = reader.ReadInt16(); + head.yMin = reader.ReadInt16(); + head.xMax = reader.ReadInt16(); + head.yMax = reader.ReadInt16(); + head.macStyle = reader.ReadUInt16(); + head.lowestRecPPEM = reader.ReadUInt16(); + head.fontDirectionHint = reader.ReadInt16(); + head.indexToLocFormat = reader.ReadInt16(); + head.glyphDataFormat = reader.ReadInt16(); + } + + /** + * glyfId到glyphData的索引 + *

    根据定义,索引零指向“丢失的字符”。 + *

    loca.length = maxp.numGlyphs + 1; + */ + private int[] loca; + + private void readLocaTable(byte[] buffer) { + var dataTable = directorys.get("loca"); + assert dataTable != null; + var reader = new BufferReader(buffer, dataTable.offset); + if (head.indexToLocFormat == 0) { + loca = reader.ReadUInt16Array(dataTable.length / 2); + // 当loca表数据长度为Uint16时,需要翻倍 + for (var i = 0; i < loca.length; i++) loca[i] *= 2; + } else { + loca = reader.ReadInt32Array(dataTable.length / 4); + } + } + + private void readCmapTable(byte[] buffer) { + var dataTable = directorys.get("cmap"); + assert dataTable != null; + var reader = new BufferReader(buffer, dataTable.offset); + Cmap.version = reader.ReadUInt16(); + Cmap.numTables = reader.ReadUInt16(); + for (int i = 0; i < Cmap.numTables; ++i) { + CmapRecord record = new CmapRecord(); + record.platformID = reader.ReadUInt16(); + record.encodingID = reader.ReadUInt16(); + record.offset = reader.ReadUInt32(); + Cmap.records.add(record); + } + + for (var formatTable : Cmap.records) { + int fmtOffset = formatTable.offset; + if (Cmap.tables.containsKey(fmtOffset)) continue; + reader.position(dataTable.offset + fmtOffset); + + CmapFormat f = new CmapFormat(); + f.format = reader.ReadUInt16(); + f.length = reader.ReadUInt16(); + f.language = reader.ReadUInt16(); + switch (f.format) { + case 0: { + f.glyphIdArray = reader.ReadUInt8Array(f.length - 6); + // 记录 unicode->glyphId 映射表 + int unicodeInclusive = 0; + int unicodeExclusive = f.glyphIdArray.length; + for (; unicodeInclusive < unicodeExclusive; unicodeInclusive++) { + if (f.glyphIdArray[unicodeInclusive] == 0) continue; // 排除轮廓索引为0的Unicode + unicodeToGlyphId.put(unicodeInclusive, f.glyphIdArray[unicodeInclusive]); + } + break; + } + case 4: { + f.segCountX2 = reader.ReadUInt16(); + int segCount = f.segCountX2 / 2; + f.searchRange = reader.ReadUInt16(); + f.entrySelector = reader.ReadUInt16(); + f.rangeShift = reader.ReadUInt16(); + f.endCode = reader.ReadUInt16Array(segCount); + f.reservedPad = reader.ReadUInt16(); + f.startCode = reader.ReadUInt16Array(segCount); + f.idDelta = reader.ReadInt16Array(segCount); + f.idRangeOffsets = reader.ReadUInt16Array(segCount); + // 一个包含字形索引的数组,其长度是任意的,取决于映射的复杂性和字体中的字符数量。 + int glyphIdArrayLength = (f.length - 16 - (segCount * 8)) / 2; + f.glyphIdArray = reader.ReadUInt16Array(glyphIdArrayLength); + + // 记录 unicode->glyphId 映射表 + for (int segmentIndex = 0; segmentIndex < segCount; segmentIndex++) { + int unicodeInclusive = f.startCode[segmentIndex]; + int unicodeExclusive = f.endCode[segmentIndex]; + int idDelta = f.idDelta[segmentIndex]; + int idRangeOffset = f.idRangeOffsets[segmentIndex]; + for (int unicode = unicodeInclusive; unicode <= unicodeExclusive; unicode++) { + int glyphId = 0; + if (idRangeOffset == 0) { + glyphId = (unicode + idDelta) & 0xFFFF; + } else { + int gIndex = (idRangeOffset / 2) + unicode - unicodeInclusive + segmentIndex - segCount; + if (gIndex < glyphIdArrayLength) glyphId = f.glyphIdArray[gIndex] + idDelta; + } + if (glyphId == 0) continue; // 排除轮廓索引为0的Unicode + unicodeToGlyphId.put(unicode, glyphId); + } + } + break; + } + case 6: { + f.firstCode = reader.ReadUInt16(); + f.entryCount = reader.ReadUInt16(); + // 范围内字符代码的字形索引值数组。 + f.glyphIdArray = reader.ReadUInt16Array(f.entryCount); + + // 记录 unicode->glyphId 映射表 + int unicodeIndex = f.firstCode; + int unicodeCount = f.entryCount; + for (int gIndex = 0; gIndex < unicodeCount; gIndex++) { + unicodeToGlyphId.put(unicodeIndex, f.glyphIdArray[gIndex]); + unicodeIndex++; + } + break; + } + default: + break; + } + Cmap.tables.put(fmtOffset, f); + } + } + + private void readMaxpTable(byte[] buffer) { + var dataTable = directorys.get("maxp"); + assert dataTable != null; + var reader = new BufferReader(buffer, dataTable.offset); + maxp.version = reader.ReadUInt32(); + maxp.numGlyphs = reader.ReadUInt16(); + maxp.maxPoints = reader.ReadUInt16(); + maxp.maxContours = reader.ReadUInt16(); + maxp.maxCompositePoints = reader.ReadUInt16(); + maxp.maxCompositeContours = reader.ReadUInt16(); + maxp.maxZones = reader.ReadUInt16(); + maxp.maxTwilightPoints = reader.ReadUInt16(); + maxp.maxStorage = reader.ReadUInt16(); + maxp.maxFunctionDefs = reader.ReadUInt16(); + maxp.maxInstructionDefs = reader.ReadUInt16(); + maxp.maxStackElements = reader.ReadUInt16(); + maxp.maxSizeOfInstructions = reader.ReadUInt16(); + maxp.maxComponentElements = reader.ReadUInt16(); + maxp.maxComponentDepth = reader.ReadUInt16(); + } + + /** + * 字形轮廓表 数组 + */ + private GlyfLayout[] glyfArray; + + private void readGlyfTable(byte[] buffer) { + var dataTable = directorys.get("glyf"); + assert dataTable != null; + int glyfCount = maxp.numGlyphs; + glyfArray = new GlyfLayout[glyfCount]; // 创建字形容器 + + var reader = new BufferReader(buffer, 0); + for (int index = 0; index < glyfCount; index++) { + if (loca[index] == loca[index + 1]) continue; // 当前loca与下一个loca相同,表示这个字形不存在 + int offset = dataTable.offset + loca[index]; + // 读GlyphHeaders + var glyph = new GlyfLayout(); + reader.position(offset); + glyph.numberOfContours = reader.ReadInt16(); + if (glyph.numberOfContours > maxp.maxContours) continue; // 如果字形轮廓数大于非复合字形中包含的最大轮廓数,则说明该字形无效。 + glyph.xMin = reader.ReadInt16(); + glyph.yMin = reader.ReadInt16(); + glyph.xMax = reader.ReadInt16(); + glyph.yMax = reader.ReadInt16(); + + // 轮廓数为0时,不需要解析轮廓数据 + if (glyph.numberOfContours == 0) continue; + // 读Glyph轮廓数据 + if (glyph.numberOfContours > 0) { + // 简单轮廓 + glyph.glyphSimple = new GlyphTableBySimple(); + glyph.glyphSimple.endPtsOfContours = reader.ReadUInt16Array(glyph.numberOfContours); + glyph.glyphSimple.instructionLength = reader.ReadUInt16(); + glyph.glyphSimple.instructions = reader.ReadUInt8Array(glyph.glyphSimple.instructionLength); + int flagLength = glyph.glyphSimple.endPtsOfContours[glyph.glyphSimple.endPtsOfContours.length - 1] + 1; + // 获取轮廓点描述标志 + glyph.glyphSimple.flags = new int[flagLength]; + for (int n = 0; n < flagLength; ++n) { + var glyphSimpleFlag = reader.ReadUInt8(); + glyph.glyphSimple.flags[n] = glyphSimpleFlag; + if ((glyphSimpleFlag & 0x08) == 0x08) { + for (int m = reader.ReadUInt8(); m > 0; --m) { + glyph.glyphSimple.flags[++n] = glyphSimpleFlag; + } + } + } + // 获取轮廓点描述x轴相对值 + glyph.glyphSimple.xCoordinates = new int[flagLength]; + for (int n = 0; n < flagLength; ++n) { + switch (glyph.glyphSimple.flags[n] & 0x12) { + case 0x02: + glyph.glyphSimple.xCoordinates[n] = -1 * reader.ReadUInt8(); + break; + case 0x12: + glyph.glyphSimple.xCoordinates[n] = reader.ReadUInt8(); + break; + case 0x10: + glyph.glyphSimple.xCoordinates[n] = 0; // 点位数据重复上一次数据,那么相对数据变化量就是0 + break; + case 0x00: + glyph.glyphSimple.xCoordinates[n] = reader.ReadInt16(); + break; + } + } + // 获取轮廓点描述y轴相对值 + glyph.glyphSimple.yCoordinates = new int[flagLength]; + for (int n = 0; n < flagLength; ++n) { + switch (glyph.glyphSimple.flags[n] & 0x24) { + case 0x04: + glyph.glyphSimple.yCoordinates[n] = -1 * reader.ReadUInt8(); + break; + case 0x24: + glyph.glyphSimple.yCoordinates[n] = reader.ReadUInt8(); + break; + case 0x20: + glyph.glyphSimple.yCoordinates[n] = 0; // 点位数据重复上一次数据,那么相对数据变化量就是0 + break; + case 0x00: + glyph.glyphSimple.yCoordinates[n] = reader.ReadInt16(); + break; + } + } + } else { + // 复合轮廓 + glyph.glyphComponent = new LinkedList<>(); + while (true) { + var glyphTableComponent = new GlyphTableComponent(); + glyphTableComponent.flags = reader.ReadUInt16(); + glyphTableComponent.glyphIndex = reader.ReadUInt16(); + switch (glyphTableComponent.flags & 0b11) { + case 0b00: + glyphTableComponent.argument1 = reader.ReadUInt8(); + glyphTableComponent.argument2 = reader.ReadUInt8(); + break; + case 0b10: + glyphTableComponent.argument1 = reader.ReadInt8(); + glyphTableComponent.argument2 = reader.ReadInt8(); + break; + case 0b01: + glyphTableComponent.argument1 = reader.ReadUInt16(); + glyphTableComponent.argument2 = reader.ReadUInt16(); + break; + case 0b11: + glyphTableComponent.argument1 = reader.ReadInt16(); + glyphTableComponent.argument2 = reader.ReadInt16(); + break; + } + switch (glyphTableComponent.flags & 0b11001000) { + case 0b00001000: + // 有单一比例 + glyphTableComponent.yScale = glyphTableComponent.xScale = ((float) reader.ReadUInt16()) / 16384.0f; + break; + case 0b01000000: + // 有X和Y的独立比例 + glyphTableComponent.xScale = ((float) reader.ReadUInt16()) / 16384.0f; + glyphTableComponent.yScale = ((float) reader.ReadUInt16()) / 16384.0f; + break; + case 0b10000000: + // 有2x2变换矩阵 + glyphTableComponent.xScale = ((float) reader.ReadUInt16()) / 16384.0f; + glyphTableComponent.scale01 = ((float) reader.ReadUInt16()) / 16384.0f; + glyphTableComponent.scale10 = ((float) reader.ReadUInt16()) / 16384.0f; + glyphTableComponent.yScale = ((float) reader.ReadUInt16()) / 16384.0f; + break; + } + glyph.glyphComponent.add(glyphTableComponent); + if ((glyphTableComponent.flags & 0x20) == 0) break; + } + } + glyfArray[index] = glyph; + } + } + + /** + * 使用轮廓索引值获取轮廓数据 + * + * @param glyfId 轮廓索引 + * @return 轮廓数据 + */ + public String getGlyfById(int glyfId) { + var glyph = glyfArray[glyfId]; + if (glyph == null) return null; // 过滤不存在的字体轮廓 + String glyphString; + if (glyph.numberOfContours >= 0) { + // 简单字形 + int dataCount = glyph.glyphSimple.flags.length; + String[] coordinateArray = new String[dataCount]; + for (int i = 0; i < dataCount; i++) { + coordinateArray[i] = glyph.glyphSimple.xCoordinates[i] + "," + glyph.glyphSimple.yCoordinates[i]; + } + glyphString = String.join("|", coordinateArray); + } else { + // 复合字形 + LinkedList glyphIdList = new LinkedList<>(); + for (var g : glyph.glyphComponent) { + glyphIdList.add("{" + + "flags:" + g.flags + "," + + "glyphIndex:" + g.glyphIndex + "," + + "arg1:" + g.argument1 + "," + + "arg2:" + g.argument2 + "," + + "xScale:" + g.xScale + "," + + "scale01:" + g.scale01 + "," + + "scale10:" + g.scale10 + "," + + "yScale:" + g.yScale + "}"); + } + glyphString = "[" + String.join(",", glyphIdList) + "]"; + } + return glyphString; + } + + /** + * 构造函数 + * + * @param buffer 传入TTF字体二进制数组 + */ + public QueryTTF(final byte[] buffer) { + var fontReader = new BufferReader(buffer, 0); +// Log.i("QueryTTF", "读文件头"); // 获取文件头 + fileHeader.sfntVersion = fontReader.ReadUInt32(); + fileHeader.numTables = fontReader.ReadUInt16(); + fileHeader.searchRange = fontReader.ReadUInt16(); + fileHeader.entrySelector = fontReader.ReadUInt16(); + fileHeader.rangeShift = fontReader.ReadUInt16(); + // 获取目录 + for (int i = 0; i < fileHeader.numTables; ++i) { + Directory d = new Directory(); + d.tableTag = new String(fontReader.ReadByteArray(4), StandardCharsets.US_ASCII); + d.checkSum = fontReader.ReadUInt32(); + d.offset = fontReader.ReadUInt32(); + d.length = fontReader.ReadUInt32(); + directorys.put(d.tableTag, d); + } + +// Log.i("QueryTTF", "解析表 name"); // 字体信息,包含版权、名称、作者等... + readNameTable(buffer); +// Log.i("QueryTTF", "解析表 head"); // 获取 head.indexToLocFormat + readHeadTable(buffer); +// Log.i("QueryTTF", "解析表 cmap"); // Unicode编码->轮廓索引 对照表 + readCmapTable(buffer); +// Log.i("QueryTTF", "解析表 loca"); // 轮廓数据偏移地址表 + readLocaTable(buffer); +// Log.i("QueryTTF", "解析表 maxp"); // 获取 maxp.numGlyphs 字体轮廓数量 + readMaxpTable(buffer); +// Log.i("QueryTTF", "解析表 glyf"); // 字体轮廓数据表,需要解析loca,maxp表后计算 + readGlyfTable(buffer); +// Log.i("QueryTTF", "建立Unicode&Glyph映射表"); + int glyfArrayLength = glyfArray.length; + for (var item : unicodeToGlyphId.entrySet()) { + int key = item.getKey(); + int val = item.getValue(); + if (val >= glyfArrayLength) continue; + String glyfString = getGlyfById(val); + unicodeToGlyph.put(key, glyfString); + if (glyfString == null) continue; // null 不能用作hashmap的key + glyphToUnicode.put(glyfString, key); + } +// Log.i("QueryTTF", "字体处理完成"); + } + + public final HashMap unicodeToGlyph = new HashMap<>(); + public final HashMap glyphToUnicode = new HashMap<>(); + public final HashMap unicodeToGlyphId = new HashMap<>(); + + /** + * 使用 Unicode 值获查询廓索引 + * + * @param unicode 传入 Unicode 值 + * @return 轮廓索引 + */ + public int getGlyfIdByUnicode(int unicode) { + var result = unicodeToGlyphId.get(unicode); + if (result == null) return 0; // 如果找不到Unicode对应的轮廓索引,就返回默认值0 + return result; + } + + /** + * 使用 Unicode 值查询轮廓数据 + * + * @param unicode 传入 Unicode 值 + * @return 轮廓数据 + */ + public String getGlyfByUnicode(int unicode) { + return unicodeToGlyph.get(unicode); + } + + /** + * 使用轮廓数据反查 Unicode 值 + * + * @param glyph 传入轮廓数据 + * @return Unicode + */ + public int getUnicodeByGlyf(String glyph) { + var result = glyphToUnicode.get(glyph); + if (result == null) return 0; // 如果轮廓数据找不到对应的Unicode,就返回默认值0 + return result; + } + + /** + * Unicode 空白字符判断 + * + * @param unicode 字符的 Unicode 值 + * @return true:是空白字符; false:非空白字符 + */ + public boolean isBlankUnicode(int unicode) { + return switch (unicode) { + case 0x0009, // 水平制表符 (Horizontal Tab) + 0x0020, // 空格 (Space) + 0x00A0, // 不中断空格 (No-Break Space) + 0x2002, // En空格 (En Space) + 0x2003, // Em空格 (Em Space) + 0x2007, // 刚性空格 (Figure Space) + 0x200A, // 发音修饰字母的连字符 (Hair Space) + 0x200B, // 零宽空格 (Zero Width Space) + 0x200C, // 零宽不连字 (Zero Width Non-Joiner) + 0x200D, // 零宽连字 (Zero Width Joiner) + 0x202F, // 狭窄不中断空格 (Narrow No-Break Space) + 0x205F // 中等数学空格 (Medium Mathematical Space) + -> true; + default -> false; + }; + } +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt new file mode 100755 index 000000000..5fd91e15d --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt @@ -0,0 +1,378 @@ +package io.legado.app.model.analyzeRule + +//通用的规则切分处理 +class RuleAnalyzer(data: String, code: Boolean = false) { + + private var queue: String = data //被处理字符串 + private var pos = 0 //当前处理到的位置 + private var start = 0 //当前处理字段的开始 + private var startX = 0 //当前规则的开始 + + private var rule = ArrayList() //分割出的规则列表 + private var step: Int = 0 //分割字符的长度 + var elementsType = "" //当前分割字符串 + + fun trim() { // 修剪当前规则之前的"@"或者空白符 + if (queue[pos] == '@' || queue[pos] < '!') { //在while里重复设置start和startX会拖慢执行速度,所以先来个判断是否存在需要修剪的字段,最后再一次性设置start和startX + pos++ + while (queue[pos] == '@' || queue[pos] < '!') pos++ + start = pos //开始点推移 + startX = pos //规则起始点推移 + } + } + + //将pos重置为0,方便复用 + fun reSetPos() { + pos = 0 + startX = 0 + } + + /** + * 从剩余字串中拉出一个字符串,直到但不包括匹配序列 + * @param seq 查找的字符串 **区分大小写** + * @return 是否找到相应字段。 + */ + private fun consumeTo(seq: String): Boolean { + start = pos //将处理到的位置设置为规则起点 + val offset = queue.indexOf(seq, pos) + return if (offset != -1) { + pos = offset + true + } else false + } + + /** + * 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 + * @param seq 匹配字符串序列 + * @return 成功返回true并设置间隔,失败则直接返回fasle + */ + private fun consumeToAny(vararg seq: String): Boolean { + + var pos = pos //声明新变量记录匹配位置,不更改类本身的位置 + + while (pos != queue.length) { + + for (s in seq) { + if (queue.regionMatches(pos, s, 0, s.length)) { + step = s.length //间隔数 + this.pos = pos //匹配成功, 同步处理位置到类 + return true //匹配就返回 true + } + } + + pos++ //逐个试探 + } + return false + } + + /** + * 从剩余字串中拉出一个字符串,直到但不包括匹配序列(匹配参数列表中一项即为匹配),或剩余字串用完。 + * @param seq 匹配字符序列 + * @return 返回匹配位置 + */ + private fun findToAny(vararg seq: Char): Int { + + var pos = pos //声明新变量记录匹配位置,不更改类本身的位置 + + while (pos != queue.length) { + + for (s in seq) if (queue[pos] == s) return pos //匹配则返回位置 + + pos++ //逐个试探 + + } + + return -1 + } + + /** + * 拉出一个非内嵌代码平衡组,存在转义文本 + */ + private fun chompCodeBalanced(open: Char, close: Char): Boolean { + + var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos + + var depth = 0 //嵌套深度 + var otherDepth = 0 //其他对称符合嵌套深度 + + var inSingleQuote = false //单引号 + var inDoubleQuote = false //双引号 + + do { + if (pos == queue.length) break + val c = queue[pos++] + if (c != ESC) { //非转义字符 + if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 + else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 + + if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 + + if (c == '[') depth++ //开始嵌套一层 + else if (c == ']') depth-- //闭合一层嵌套 + else if (depth == 0) { + //处于默认嵌套中的非默认字符不需要平衡,仅depth为0时默认嵌套全部闭合,此字符才进行嵌套 + if (c == open) otherDepth++ + else if (c == close) otherDepth-- + } + + } else pos++ + + } while (depth > 0 || otherDepth > 0) //拉出一个平衡字串 + + return if (depth > 0 || otherDepth > 0) false else { + this.pos = pos //同步位置 + true + } + } + + /** + * 拉出一个规则平衡组,经过仔细测试xpath和jsoup中,引号内转义字符无效。 + */ + private fun chompRuleBalanced(open: Char, close: Char): Boolean { + + var pos = pos //声明临时变量记录匹配位置,匹配成功后才同步到类的pos + var depth = 0 //嵌套深度 + var inSingleQuote = false //单引号 + var inDoubleQuote = false //双引号 + + do { + if (pos == queue.length) break + val c = queue[pos++] + if (c == '\'' && !inDoubleQuote) inSingleQuote = !inSingleQuote //匹配具有语法功能的单引号 + else if (c == '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote //匹配具有语法功能的双引号 + + if (inSingleQuote || inDoubleQuote) continue //语法单元未匹配结束,直接进入下个循环 + else if (c == '\\') { //不在引号中的转义字符才将下个字符转义 + pos++ + continue + } + + if (c == open) depth++ //开始嵌套一层 + else if (c == close) depth-- //闭合一层嵌套 + + } while (depth > 0) //拉出一个平衡字串 + + return if (depth > 0) false else { + this.pos = pos //同步位置 + true + } + } + + /** + * 不用正则,不到最后不切片也不用中间变量存储,只在序列中标记当前查找字段的开头结尾,到返回时才切片,高效快速准确切割规则 + * 解决jsonPath自带的"&&"和"||"与阅读的规则冲突,以及规则正则或字符串中包含"&&"、"||"、"%%"、"@"导致的冲突 + */ + tailrec fun splitRule(vararg split: String): ArrayList { //首段匹配,elementsType为空 + + if (split.size == 1) { + elementsType = split[0] //设置分割字串 + return if (!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + } else { + step = elementsType.length //设置分隔符长度 + splitRule() + } //递归匹配 + } else if (!consumeToAny(* split)) { //未找到分隔符 + rule += queue.substring(startX) + return rule + } + + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 + + do { + val st = findToAny('[', '(') //查找筛选器位置 + + if (st == -1) { + + rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + rule += queue.substring(pos) //将剩余字段压入数组末尾 + + return rule + } + + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + + rule = arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + + elementsType = queue.substring(end, end + step) //设置组合类型 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + return if (pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } + } + + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring(0, start) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 + + } while (end > pos) + + start = pos //设置开始查找筛选器位置的起始位置 + + return splitRule(* split) //递归调用首段匹配 + } + + @JvmName("splitRuleNext") + private tailrec fun splitRule(): ArrayList { //二段匹配被调用,elementsType非空(已在首段赋值),直接按elementsType查找,比首段采用的方式更快 + + val end = pos //记录分隔位置 + pos = start //重回开始,启动另一种查找 + + do { + val st = findToAny('[', '(') //查找筛选器位置 + + if (st == -1) { + + rule += arrayOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType)) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + rule += queue.substring(pos) //将剩余字段压入数组末尾 + + return rule + } + + if (st > end) { //先匹配到st1pos,表明分隔字串不在选择器中,将选择器前分隔字串分隔的字段依次压入数组 + + rule += arrayListOf(queue.substring(startX, end)) //压入分隔的首段规则到数组 + pos = end + step //跳过分隔符 + + while (consumeTo(elementsType) && pos < st) { //循环切分规则压入数组 + rule += queue.substring(start, pos) + pos += step //跳过分隔符 + } + + return if (pos > st) { + startX = start + splitRule() //首段已匹配,但当前段匹配未完成,调用二段匹配 + } else { //执行到此,证明后面再无分隔字符 + rule += queue.substring(pos) //将剩余字段压入数组末尾 + rule + } + } + + pos = st //位置推移到筛选器处 + val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 + + if (!chompBalanced(queue[pos], next)) throw Error( + queue.substring(0, start) + "后未平衡" + ) //拉出一个筛选器,不平衡则报错 + + } while (end > pos) + + start = pos //设置开始查找筛选器位置的起始位置 + + return if (!consumeTo(elementsType)) { + rule += queue.substring(startX) + rule + } else splitRule() //递归匹配 + + } + + /** + * 替换内嵌规则 + * @param inner 起始标志,如{$. + * @param startStep 不属于规则部分的前置字符长度,如{$.中{不属于规则的组成部分,故startStep为1 + * @param endStep 不属于规则部分的后置字符长度 + * @param fr 查找到内嵌规则时,用于解析的函数 + * + * */ + fun innerRule( + inner: String, + startStep: Int = 1, + endStep: Int = 1, + fr: (String) -> String? + ): String { + val st = StringBuilder() + + while (consumeTo(inner)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true + val posPre = pos //记录consumeTo匹配位置 + if (chompCodeBalanced('{', '}')) { + val frv = fr(queue.substring(posPre + startStep, pos - endStep)) + if (!frv.isNullOrEmpty()) { + st.append(queue.substring(startX, posPre) + frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 + startX = pos //记录下次规则起点 + continue //获取内容成功,继续选择下个内嵌规则 + } + } + pos += inner.length //拉出字段不平衡,inner只是个普通字串,跳到此inner后继续匹配 + } + + return if (startX == 0) "" else st.apply { + append(queue.substring(startX)) + }.toString() + } + + /** + * 替换内嵌规则 + * @param fr 查找到内嵌规则时,用于解析的函数 + * + * */ + fun innerRule( + startStr: String, + endStr: String, + fr: (String) -> String? + ): String { + + val st = StringBuilder() + while (consumeTo(startStr)) { //拉取成功返回true,ruleAnalyzes里的字符序列索引变量pos后移相应位置,否则返回false,且isEmpty为true + pos += startStr.length //跳过开始字符串 + val posPre = pos //记录consumeTo匹配位置 + if (consumeTo(endStr)) { + val frv = fr(queue.substring(posPre, pos)) + st.append( + queue.substring( + startX, + posPre - startStr.length + ) + frv + ) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 + pos += endStr.length //跳过结束字符串 + startX = pos //记录下次规则起点 + } + } + + return if (startX == 0) queue else st.apply { + append(queue.substring(startX)) + }.toString() + } + + //设置平衡组函数,json或JavaScript时设置成chompCodeBalanced,否则为chompRuleBalanced + val chompBalanced = if (code) ::chompCodeBalanced else ::chompRuleBalanced + + companion object { + + /** + * 转义字符 + */ + private const val ESC = '\\' + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt new file mode 100755 index 000000000..ba17c882e --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleData.kt @@ -0,0 +1,30 @@ +package io.legado.app.model.analyzeRule + +import io.legado.app.utils.GSON + +class RuleData : RuleDataInterface { + + override val variableMap by lazy { + hashMapOf() + } + + override fun putBigVariable(key: String, value: String?) { + if (value == null) { + variableMap.remove(key) + } else { + variableMap[key] = value + } + } + + override fun getBigVariable(key: String): String? { + return null + } + + fun getVariable(): String? { + if (variableMap.isEmpty()) { + return null + } + return GSON.toJson(variableMap) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt b/app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt new file mode 100755 index 000000000..c7a58ce40 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt @@ -0,0 +1,38 @@ +package io.legado.app.model.analyzeRule + +interface RuleDataInterface { + + val variableMap: HashMap + + fun putVariable(key: String, value: String?): Boolean { + val keyExist = variableMap.contains(key) + return when { + value == null -> { + variableMap.remove(key) + putBigVariable(key, null) + keyExist + } + + value.length < 10000 -> { + putBigVariable(key, null) + variableMap[key] = value + true + } + + else -> { + variableMap.remove(key) + putBigVariable(key, value) + keyExist + } + } + } + + fun putBigVariable(key: String, value: String?) + + fun getVariable(key: String): String { + return variableMap[key] ?: getBigVariable(key) ?: "" + } + + fun getBigVariable(key: String): String? + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/BaseLocalBookParse.kt b/app/src/main/java/io/legado/app/model/localBook/BaseLocalBookParse.kt new file mode 100755 index 000000000..8d59bfc23 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/BaseLocalBookParse.kt @@ -0,0 +1,21 @@ +package io.legado.app.model.localBook + +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import java.io.InputStream + +/** + *companion object interface + *see EpubFile.kt + */ +interface BaseLocalBookParse { + + fun upBookInfo(book: Book) + + fun getChapterList(book: Book): ArrayList + + fun getContent(book: Book, chapter: BookChapter): String? + + fun getImage(book: Book, href: String): InputStream? + +} diff --git a/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt new file mode 100755 index 000000000..eb0f7f61d --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/EpubFile.kt @@ -0,0 +1,438 @@ +package io.legado.app.model.localBook + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.ParcelFileDescriptor +import android.text.TextUtils +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.BookHelp +import io.legado.app.utils.FileUtils +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.encodeURI +import io.legado.app.utils.isXml +import io.legado.app.utils.printOnDebug +import me.ag2s.epublib.domain.EpubBook +import me.ag2s.epublib.domain.Resource +import me.ag2s.epublib.domain.TOCReference +import me.ag2s.epublib.epub.EpubReader +import me.ag2s.epublib.util.zip.AndroidZipFile +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.parser.Parser +import org.jsoup.select.Elements +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.URI +import java.net.URLDecoder +import java.nio.charset.Charset + +class EpubFile(var book: Book) { + + companion object : BaseLocalBookParse { + private var eFile: EpubFile? = null + + @Synchronized + private fun getEFile(book: Book): EpubFile { + if (eFile == null || eFile?.book?.bookUrl != book.bookUrl) { + eFile = EpubFile(book) + //对于Epub文件默认不启用替换 + //io.legado.app.data.entities.Book getUseReplaceRule + return eFile!! + } + eFile?.book = book + return eFile!! + } + + @Synchronized + override fun getChapterList(book: Book): ArrayList { + return getEFile(book).getChapterList() + } + + @Synchronized + override fun getContent(book: Book, chapter: BookChapter): String? { + return getEFile(book).getContent(chapter) + } + + @Synchronized + override fun getImage( + book: Book, + href: String + ): InputStream? { + return getEFile(book).getImage(href) + } + + @Synchronized + override fun upBookInfo(book: Book) { + return getEFile(book).upBookInfo() + } + + fun clear() { + eFile = null + } + } + + private var mCharset: Charset = Charset.defaultCharset() + + /** + *持有引用,避免被回收 + */ + private var fileDescriptor: ParcelFileDescriptor? = null + private var epubBook: EpubBook? = null + get() { + if (field == null || fileDescriptor == null) { + field = readEpub() + } + return field + } + private var epubBookContents: List? = null + get() { + if (field == null || fileDescriptor == null) { + field = epubBook?.contents + } + return field + } + + init { + upBookCover(true) + } + + /** + * 重写epub文件解析代码,直接读出压缩包文件生成Resources给epublib,这样的好处是可以逐一修改某些文件的格式错误 + */ + private fun readEpub(): EpubBook? { + return kotlin.runCatching { + //ContentScheme拷贝到私有文件夹采用懒加载防止OOM + //val zipFile = BookHelp.getEpubFile(book) + BookHelp.getBookPFD(book)?.let { + fileDescriptor = it + val zipFile = AndroidZipFile(it, book.originName) + EpubReader().readEpubLazy(zipFile, "utf-8") + } + + + }.onFailure { + AppLog.put("读取Epub文件失败\n${it.localizedMessage}", it) + it.printOnDebug() + }.getOrThrow() + } + + private fun getContent(chapter: BookChapter): String? { + /*获取当前章节文本*/ + val contents = epubBookContents ?: return null + val nextChapterFirstResourceHref = chapter.getVariable("nextUrl").substringBeforeLast("#") + val currentChapterFirstResourceHref = chapter.url.substringBeforeLast("#") + val isLastChapter = nextChapterFirstResourceHref.isBlank() + val startFragmentId = chapter.startFragmentId + val endFragmentId = chapter.endFragmentId + val elements = Elements() + var findChapterFirstSource = false + val includeNextChapterResource = !endFragmentId.isNullOrBlank() + /*一些书籍依靠href索引的resource会包含多个章节,需要依靠fragmentId来截取到当前章节的内容*/ + /*注:这里较大增加了内容加载的时间,所以首次获取内容后可存储到本地cache,减少重复加载*/ + for (res in contents) { + if (!findChapterFirstSource) { + if (currentChapterFirstResourceHref != res.href) continue + findChapterFirstSource = true + // 第一个xhtml文件 + elements.add( + getBody(res, startFragmentId, endFragmentId) + ) + // 不是最后章节 且 已经遍历到下一章节的内容时停止 + if (!isLastChapter && res.href == nextChapterFirstResourceHref) break + continue + } + if (nextChapterFirstResourceHref != res.href) { + // 其余部分 + elements.add(getBody(res, null, null)) + } else { + // 下一章节的第一个xhtml + if (includeNextChapterResource) { + //有Fragment 则添加到上一章节 + elements.add(getBody(res, null, endFragmentId)) + } + break + } + } + //title标签中的内容不需要显示在正文中,去除 + elements.select("title").remove() + elements.select("[style*=display:none]").remove() + elements.select("img[src=\"cover.jpeg\"]").forEachIndexed { i, it -> + if (i > 0) it.remove() + } + elements.select("img").forEach { + if (it.attributesSize() <= 1) { + return@forEach + } + val src = it.attr("src") + it.clearAttributes() + it.attr("src", src) + } + val tag = Book.rubyTag + if (book.getDelTag(tag)) { + elements.select("rp, rt").remove() + } + val html = elements.outerHtml() + return HtmlFormatter.formatKeepImg(html) + } + + private fun getBody(res: Resource, startFragmentId: String?, endFragmentId: String?): Element { + /** + * + * ...titlepage.xhtml + * 大多数epub文件的封面页都会带有cover,可以一定程度上解决封面读取问题 + */ + if (res.href.contains("titlepage.xhtml") || + res.href.contains("cover") + ) { + return Jsoup.parseBodyFragment("") + } + + // Jsoup可能会修复不规范的xhtml文件 解析处理后再获取 + var bodyElement = Jsoup.parse(String(res.data, mCharset)).body() + bodyElement.children().run { + select("script").remove() + select("style").remove() + } + // 获取body对应的文本 + var bodyString = bodyElement.outerHtml() + val originBodyString = bodyString + /** + * 某些xhtml文件 章节标题和内容不在一个节点或者不是兄弟节点 + *

    + * 目录2 + *
    + *

    ....

    + * 先找到FragmentId对应的Element 然后直接截取之间的html + */ + if (!startFragmentId.isNullOrBlank()) { + bodyElement.getElementById(startFragmentId)?.outerHtml()?.let { + val tagStart = it.substringBefore("\n") + bodyString = tagStart + bodyString.substringAfter(tagStart) + } + } + if (!endFragmentId.isNullOrBlank() && endFragmentId != startFragmentId) { + bodyElement.getElementById(endFragmentId)?.outerHtml()?.let { + val tagStart = it.substringBefore("\n") + bodyString = bodyString.substringBefore(tagStart) + } + } + //截取过再重新解析 + if (bodyString != originBodyString) { + bodyElement = Jsoup.parse(bodyString).body() + } + /*选择去除正文中的H标签,部分书籍标题与阅读标题重复待优化*/ + val tag = Book.hTag + if (book.getDelTag(tag)) { + bodyElement.run { + select("h1, h2, h3, h4, h5, h6").remove() + //getElementsMatchingOwnText(chapter.title)?.remove() + } + } + bodyElement.select("image").forEach { + it.tagName("img", Parser.NamespaceHtml) + it.attr("src", it.attr("xlink:href")) + } + bodyElement.select("img").forEach { + val src = it.attr("src").trim().encodeURI() + val href = res.href.encodeURI() + val resolvedHref = URLDecoder.decode(URI(href).resolve(src).toString(), "UTF-8") + it.attr("src", resolvedHref) + } + return bodyElement + } + + private fun getImage(href: String): InputStream? { + if (href == "cover.jpeg") return epubBook?.coverImage?.inputStream + val abHref = URLDecoder.decode(href, "UTF-8") + return epubBook?.resources?.getByHref(abHref)?.inputStream + } + + private fun upBookCover(fastCheck: Boolean = false) { + try { + epubBook?.let { + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = LocalBook.getCoverPath(book) + } + if (fastCheck && File(book.coverUrl!!).exists()) { + return + } + /*部分书籍DRM处理后,封面获取异常,待优化*/ + it.coverImage?.inputStream?.use { input -> + val cover = BitmapFactory.decodeStream(input) + val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)) + cover.compress(Bitmap.CompressFormat.JPEG, 90, out) + out.flush() + out.close() + } ?: AppLog.putDebug("Epub: 封面获取为空. path: ${book.bookUrl}") + } + } catch (e: Exception) { + AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e) + e.printOnDebug() + } + } + + private fun upBookInfo() { + if (epubBook == null) { + eFile = null + book.intro = "书籍导入异常" + } else { + upBookCover() + val metadata = epubBook!!.metadata + book.name = metadata.firstTitle + if (book.name.isEmpty()) { + book.name = book.originName.replace(".epub", "") + } + + if (metadata.authors.isNotEmpty()) { + val author = + metadata.authors[0].toString().replace("^, |, $".toRegex(), "") + book.author = author + } + if (metadata.descriptions.isNotEmpty()) { + val desc = metadata.descriptions[0] + book.intro = if (desc.isXml()) { + Jsoup.parse(metadata.descriptions[0]).text() + } else { + desc + } + } + } + } + + private fun getChapterList(): ArrayList { + val chapterList = ArrayList() + epubBook?.let { eBook -> + val refs = eBook.tableOfContents.tocReferences + if (refs == null || refs.isEmpty()) { + AppLog.putDebug("Epub: NCX file parse error, check the file: ${book.bookUrl}") + val spineReferences = eBook.spine.spineReferences + var i = 0 + val size = spineReferences.size + while (i < size) { + val resource = spineReferences[i].resource + var title = resource.title + if (TextUtils.isEmpty(title)) { + try { + val doc = + Jsoup.parse(String(resource.data, mCharset)) + val elements = doc.getElementsByTag("title") + if (elements.isNotEmpty()) { + title = elements[0].text() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + val chapter = BookChapter() + chapter.index = i + chapter.bookUrl = book.bookUrl + chapter.url = resource.href + if (i == 0 && title.isEmpty()) { + chapter.title = "封面" + } else { + chapter.title = title + } + chapterList.lastOrNull()?.putVariable("nextUrl", chapter.url) + chapterList.add(chapter) + i++ + } + } else { + parseFirstPage(chapterList, refs) + parseMenu(chapterList, refs, 0) + for (i in chapterList.indices) { + chapterList[i].index = i + } + } + } + return chapterList + } + + /*获取书籍起始页内容。部分书籍第一章之前存在封面,引言,扉页等内容*/ + /*tile获取不同书籍风格杂乱,格式化处理待优化*/ + private var durIndex = 0 + private fun parseFirstPage( + chapterList: ArrayList, + refs: List? + ) { + val contents = epubBook?.contents + if (epubBook == null || contents == null || refs == null) return + val firstRef = refs.firstOrNull { it.resource != null } ?: return + var i = 0 + durIndex = 0 + while (i < contents.size) { + val content = contents[i] + if (!content.mediaType.toString().contains("htm")) { + i++ + continue + } + /** + * 检索到第一章href停止 + * completeHref可能有fragment(#id) 必须去除 + * fix https://github.com/gedoor/legado/issues/1932 + */ + if (firstRef.completeHref.substringBeforeLast("#") == content.href) break + val chapter = BookChapter() + var title = content.title + if (TextUtils.isEmpty(title)) { + val elements = Jsoup.parse( + String(epubBook!!.resources.getByHref(content.href).data, mCharset) + ).getElementsByTag("title") + title = + if (elements.isNotEmpty() && elements[0].text().isNotBlank()) + elements[0].text() + else + "--卷首--" + } + chapter.bookUrl = book.bookUrl + chapter.title = title + chapter.url = content.href + chapter.startFragmentId = + if (content.href.substringAfter("#") == content.href) null + else content.href.substringAfter("#") + + chapterList.lastOrNull()?.endFragmentId = chapter.startFragmentId + chapterList.lastOrNull()?.putVariable("nextUrl", chapter.url) + chapterList.add(chapter) + durIndex++ + i++ + } + } + + private fun parseMenu( + chapterList: ArrayList, + refs: List?, + level: Int + ) { + refs?.forEach { ref -> + if (ref.resource != null) { + val chapter = BookChapter() + chapter.bookUrl = book.bookUrl + chapter.title = ref.title + chapter.url = ref.completeHref + chapter.startFragmentId = ref.fragmentId + chapterList.lastOrNull()?.endFragmentId = chapter.startFragmentId + chapterList.lastOrNull()?.putVariable("nextUrl", chapter.url) + chapterList.add(chapter) + durIndex++ + } + if (ref.children != null && ref.children.isNotEmpty()) { + chapterList.lastOrNull()?.isVolume = true + parseMenu(chapterList, ref.children, level + 1) + } + } + } + + + protected fun finalize() { + fileDescriptor?.close() + } + +} diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt new file mode 100755 index 000000000..233a0aae4 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -0,0 +1,518 @@ +package io.legado.app.model.localBook + +import android.net.Uri +import android.util.Base64 +import androidx.documentfile.provider.DocumentFile +import com.script.ScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.BookType +import io.legado.app.data.appDb +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.exception.EmptyFileException +import io.legado.app.exception.NoBooksDirException +import io.legado.app.exception.NoStackTraceException +import io.legado.app.exception.TocEmptyException +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.addType +import io.legado.app.help.book.archiveName +import io.legado.app.help.book.getArchiveUri +import io.legado.app.help.book.getLocalUri +import io.legado.app.help.book.getRemoteUrl +import io.legado.app.help.book.isArchive +import io.legado.app.help.book.isEpub +import io.legado.app.help.book.isMobi +import io.legado.app.help.book.isPdf +import io.legado.app.help.book.isUmd +import io.legado.app.help.book.removeLocalUriCache +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.webdav.WebDav +import io.legado.app.lib.webdav.WebDavException +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getFile +import io.legado.app.utils.inputStream +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.isDataUrl +import io.legado.app.utils.printOnDebug +import kotlinx.coroutines.runBlocking +import org.apache.commons.text.StringEscapeUtils +import splitties.init.appCtx +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.InputStream +import java.util.regex.Pattern +import kotlin.coroutines.coroutineContext + +/** + * 书籍文件导入 目录正文解析 + * 支持在线文件(txt epub umd 压缩文件 本地文件 + */ +object LocalBook { + + private val nameAuthorPatterns = arrayOf( + Pattern.compile("(.*?)《([^《》]+)》.*?作者:(.*)"), + Pattern.compile("(.*?)《([^《》]+)》(.*)"), + Pattern.compile("(^)(.+) 作者:(.+)$"), + Pattern.compile("(^)(.+) by (.+)$") + ) + + @Throws(FileNotFoundException::class, SecurityException::class) + fun getBookInputStream(book: Book): InputStream { + val uri = book.getLocalUri() + val inputStream = uri.inputStream(appCtx).getOrNull() + ?: let { + book.removeLocalUriCache() + val localArchiveUri = book.getArchiveUri() + val webDavUrl = book.getRemoteUrl() + if (localArchiveUri != null) { + // 重新导入对应的压缩包 + importArchiveFile(localArchiveUri, book.originName) { + it.contains(book.originName) + }.firstOrNull()?.let { + getBookInputStream(it) + } + } else if (webDavUrl != null && downloadRemoteBook(book)) { + // 下载远程链接 + getBookInputStream(book) + } else { + null + } + } + if (inputStream != null) return inputStream + book.removeLocalUriCache() + throw FileNotFoundException("${uri.path} 文件不存在") + } + + fun getLastModified(book: Book): Result { + return kotlin.runCatching { + val uri = Uri.parse(book.bookUrl) + if (uri.isContentScheme()) { + return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified() + } + val file = File(uri.path!!) + if (file.exists()) { + return@runCatching file.lastModified() + } + throw FileNotFoundException("${uri.path} 文件不存在") + } + } + + @Throws(TocEmptyException::class) + fun getChapterList(book: Book): ArrayList { + val chapters = when { + book.isEpub -> { + EpubFile.getChapterList(book) + } + + book.isUmd -> { + UmdFile.getChapterList(book) + } + + book.isPdf -> { + PdfFile.getChapterList(book) + } + + book.isMobi -> { + MobiFile.getChapterList(book) + } + + else -> { + TextFile.getChapterList(book) + } + } + if (chapters.isEmpty()) { + throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty)) + } + val list = ArrayList(LinkedHashSet(chapters)) + list.forEachIndexed { index, bookChapter -> + bookChapter.index = index + if (bookChapter.title.isEmpty()) { + bookChapter.title = "无标题章节" + } + } + val replaceRules = ContentProcessor.get(book).getTitleReplaceRules() + book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() } + .getDisplayTitle(replaceRules, book.getUseReplaceRule()) + book.latestChapterTitle = + list.getOrElse(book.simulatedTotalChapterNum() - 1) { list.last() } + .getDisplayTitle(replaceRules, book.getUseReplaceRule()) + book.totalChapterNum = list.size + book.latestChapterTime = System.currentTimeMillis() + return list + } + + fun getContent(book: Book, chapter: BookChapter): String? { + var content = try { + when { + book.isEpub -> { + EpubFile.getContent(book, chapter) + } + + book.isUmd -> { + UmdFile.getContent(book, chapter) + } + + book.isPdf -> { + PdfFile.getContent(book, chapter) + } + + book.isMobi -> { + MobiFile.getContent(book, chapter) + } + + else -> { + TextFile.getContent(book, chapter) + } + } + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("获取本地书籍内容失败\n${e.localizedMessage}", e) + "获取本地书籍内容失败\n${e.localizedMessage}" + } + if (book.isEpub) { + content ?: return null + if (content.indexOf('&') > -1) { + content = content.replace("<img", "< img", true) + return StringEscapeUtils.unescapeHtml4(content) + } + } + + if (content.isNullOrEmpty()) { + return null + } + + return content + } + + fun getCoverPath(book: Book): String { + return getCoverPath(book.bookUrl) + } + + private fun getCoverPath(bookUrl: String): String { + return FileUtils.getPath( + appCtx.externalFiles, + "covers", + "${MD5Utils.md5Encode16(bookUrl)}.jpg" + ) + } + + /** + * 下载在线的文件并自动导入到阅读(txt umd epub) + */ + suspend fun importFileOnLine( + str: String, + fileName: String, + source: BaseSource? = null, + ): Book { + return importFile(saveBookFile(str, fileName, source)) + } + + /** + * 导入本地文件 + */ + fun importFile(uri: Uri): Book { + val bookUrl: String + //updateTime变量不要修改,否则会导致读取不到缓存 + val (fileName, _, _, updateTime, _) = FileDoc.fromUri(uri, false).apply { + if (size == 0L) throw EmptyFileException("Unexpected empty File") + + bookUrl = toString() + } + var book = appDb.bookDao.getBook(bookUrl) + if (book == null) { + val nameAuthor = analyzeNameAuthor(fileName) + book = Book( + type = BookType.text or BookType.local, + bookUrl = bookUrl, + name = nameAuthor.first, + author = nameAuthor.second, + originName = fileName, + latestChapterTime = updateTime, + order = appDb.bookDao.minOrder - 1 + ) + upBookInfo(book) + appDb.bookDao.insert(book) + } else { + deleteBook(book, false) + upBookInfo(book) + // 触发 isLocalModified + book.latestChapterTime = 0 + //已有书籍说明是更新,删除原有目录 + appDb.bookChapterDao.delByBook(bookUrl) + } + return book + } + + fun upBookInfo(book: Book) { + when { + book.isEpub -> EpubFile.upBookInfo(book) + book.isUmd -> UmdFile.upBookInfo(book) + book.isPdf -> PdfFile.upBookInfo(book) + book.isMobi -> MobiFile.upBookInfo(book) + } + } + + /* 导入压缩包内的书籍 */ + fun importArchiveFile( + archiveFileUri: Uri, + saveFileName: String? = null, + filter: ((String) -> Boolean)? = null + ): List { + val archiveFileDoc = FileDoc.fromUri(archiveFileUri, false) + val files = ArchiveUtils.deCompress(archiveFileDoc, filter = filter) + if (files.isEmpty()) { + throw NoStackTraceException(appCtx.getString(R.string.unsupport_archivefile_entry)) + } + return files.map { + saveBookFile(FileInputStream(it), saveFileName ?: it.name).let { uri -> + importFile(uri).apply { + //附加压缩包名称 以便解压文件被删后再解压 + origin = "${BookType.localTag}::${archiveFileDoc.name}" + addType(BookType.archive) + save() + } + } + } + } + + /* 批量导入 支持自动导入压缩包的支持书籍 */ + fun importFiles(uri: Uri): List { + val books = mutableListOf() + val fileDoc = FileDoc.fromUri(uri, false) + if (ArchiveUtils.isArchive(fileDoc.name)) { + books.addAll( + importArchiveFile(uri) { + it.matches(AppPattern.bookFileRegex) + } + ) + } else { + books.add(importFile(uri)) + } + return books + } + + fun importFiles(uris: List) { + var errorCount = 0 + uris.forEach { uri -> + val fileDoc = FileDoc.fromUri(uri, false) + kotlin.runCatching { + if (ArchiveUtils.isArchive(fileDoc.name)) { + importArchiveFile(uri) { + it.matches(AppPattern.bookFileRegex) + } + } else { + importFile(uri) + } + }.onFailure { + AppLog.put("ImportFile Error:\nFile $fileDoc\n${it.localizedMessage}", it) + errorCount += 1 + } + } + if (errorCount == uris.size) { + throw NoStackTraceException("ImportFiles Error:\nAll input files occur error") + } + } + + /** + * 从文件分析书籍必要信息(书名 作者等) + */ + private fun analyzeNameAuthor(fileName: String): Pair { + val tempFileName = fileName.substringBeforeLast(".") + var name = "" + var author = "" + if (!AppConfig.bookImportFileName.isNullOrBlank()) { + try { + //在用户脚本后添加捕获author、name的代码,只要脚本中author、name有值就会被捕获 + val js = + AppConfig.bookImportFileName + "\nJSON.stringify({author:author,name:name})" + //在脚本中定义如何分解文件名成书名、作者名 + val jsonStr = RhinoScriptEngine.run { + val bindings = ScriptBindings() + bindings["src"] = tempFileName + eval(js, bindings) + }.toString() + val bookMess = GSON.fromJsonObject>(jsonStr) + .getOrThrow() + name = bookMess["name"] ?: "" + author = bookMess["author"]?.takeIf { it.length != tempFileName.length } ?: "" + } catch (e: Exception) { + AppLog.put("执行导入文件名规则出错\n${e.localizedMessage}", e) + } + } + if (name.isBlank()) { + for (pattern in nameAuthorPatterns) { + pattern.matcher(tempFileName).takeIf { it.find() }?.run { + name = group(2)!! + val group1 = group(1) ?: "" + val group3 = group(3) ?: "" + author = BookHelp.formatBookAuthor(group1 + group3) + return Pair(name, author) + } + } + name = BookHelp.formatBookName(tempFileName) + author = BookHelp.formatBookAuthor(tempFileName.replace(name, "")) + .takeIf { it.length != tempFileName.length } ?: "" + } + return Pair(name, author) + } + + fun deleteBook(book: Book, deleteOriginal: Boolean) { + kotlin.runCatching { + BookHelp.clearCache(book) + if (!book.coverUrl.isNullOrEmpty()) { + FileUtils.delete(book.coverUrl!!) + } + if (deleteOriginal) { + if (book.bookUrl.isContentScheme()) { + val uri = Uri.parse(book.bookUrl) + DocumentFile.fromSingleUri(appCtx, uri)?.delete() + } else { + FileUtils.delete(book.bookUrl) + } + } + } + } + + /** + * 下载在线的文件 + */ + suspend fun saveBookFile( + str: String, + fileName: String, + source: BaseSource? = null, + ): Uri { + AppConfig.defaultBookTreeUri + ?: throw NoBooksDirException() + val inputStream = when { + str.isAbsUrl() -> AnalyzeUrl( + str, source = source, callTimeout = 0, + coroutineContext = coroutineContext + ).getInputStreamAwait() + + str.isDataUrl() -> ByteArrayInputStream( + Base64.decode( + str.substringAfter("base64,"), + Base64.DEFAULT + ) + ) + + else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL") + } + return saveBookFile(inputStream, fileName) + } + + @Throws(SecurityException::class) + fun saveBookFile( + inputStream: InputStream, + fileName: String + ): Uri { + inputStream.use { + val defaultBookTreeUri = AppConfig.defaultBookTreeUri + if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException() + val treeUri = Uri.parse(defaultBookTreeUri) + return if (treeUri.isContentScheme()) { + val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri) + var doc = treeDoc!!.findFile(fileName) + if (doc == null) { + doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName) + ?: throw SecurityException("请重新设置书籍保存位置\nPermission Denial") + } + appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream -> + it.copyTo(oStream) + } + doc.uri + } else { + try { + val treeFile = File(treeUri.path!!) + val file = treeFile.getFile(fileName) + FileOutputStream(file).use { oStream -> + it.copyTo(oStream) + } + Uri.fromFile(file) + } catch (e: FileNotFoundException) { + throw SecurityException("请重新设置书籍保存位置\nPermission Denial\n$e").apply { + addSuppressed(e) + } + } + } + } + } + + fun isOnBookShelf( + fileName: String + ): Boolean { + return appDb.bookDao.hasFile(fileName) == true + } + + //文件类书源 合并在线书籍信息 在线 > 本地 + fun mergeBook(localBook: Book, onLineBook: Book?): Book { + onLineBook ?: return localBook + localBook.name = onLineBook.name.ifBlank { localBook.name } + localBook.author = onLineBook.author.ifBlank { localBook.author } + localBook.coverUrl = onLineBook.coverUrl + localBook.intro = + if (onLineBook.intro.isNullOrBlank()) localBook.intro else onLineBook.intro + localBook.save() + return localBook + } + + //下载book对应的远程文件 并更新Book + private fun downloadRemoteBook(localBook: Book): Boolean { + val webDavUrl = localBook.getRemoteUrl() + if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File") + try { + AppConfig.defaultBookTreeUri + ?: throw NoBooksDirException() + // 兼容旧版链接 + val webdav: WebDav = kotlin.runCatching { + WebDav.fromPath(webDavUrl) + }.getOrElse { + AppWebDav.authorization?.let { WebDav(webDavUrl, it) } + ?: throw WebDavException("Unexpected defaultBookWebDav") + } + val inputStream = runBlocking { + webdav.downloadInputStream() + } + inputStream.use { + if (localBook.isArchive) { + // 压缩包 + val archiveUri = saveBookFile(it, localBook.archiveName) + val newBook = importArchiveFile(archiveUri, localBook.originName) { name -> + name.contains(localBook.originName) + }.first() + localBook.origin = newBook.origin + localBook.bookUrl = newBook.bookUrl + } else { + // txt epub pdf umd + val fileUri = saveBookFile(it, localBook.originName) + localBook.bookUrl = FileDoc.fromUri(fileUri, false).toString() + localBook.save() + } + } + return true + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("自动下载webDav书籍失败", e) + return false + } + } + +} diff --git a/app/src/main/java/io/legado/app/model/localBook/MobiFile.kt b/app/src/main/java/io/legado/app/model/localBook/MobiFile.kt new file mode 100755 index 000000000..4b1bb4818 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/MobiFile.kt @@ -0,0 +1,318 @@ +package io.legado.app.model.localBook + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.ParcelFileDescriptor +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.BookHelp +import io.legado.app.lib.mobi.KF6Book +import io.legado.app.lib.mobi.KF8Book +import io.legado.app.lib.mobi.MobiBook +import io.legado.app.lib.mobi.MobiReader +import io.legado.app.lib.mobi.entities.TOC +import io.legado.app.utils.FileUtils +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.printOnDebug +import org.jsoup.Jsoup +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +class MobiFile(var book: Book) { + + companion object : BaseLocalBookParse { + private var mFile: MobiFile? = null + private val xmlDeclarationRegex = "<\\?xml[^>]*>".toRegex() + private val doctypeDeclarationRegex = "]*>".toRegex() + + @Synchronized + private fun getMFile(book: Book): MobiFile { + if (mFile == null || mFile?.book?.bookUrl != book.bookUrl) { + mFile = MobiFile(book) + return mFile!! + } + mFile?.book = book + return mFile!! + } + + @Synchronized + override fun getChapterList(book: Book): ArrayList { + return getMFile(book).getChapterList() + } + + @Synchronized + override fun getContent(book: Book, chapter: BookChapter): String? { + return getMFile(book).getContent(chapter) + } + + @Synchronized + override fun getImage(book: Book, href: String): InputStream? { + return getMFile(book).getImage(href) + } + + @Synchronized + override fun upBookInfo(book: Book) { + return getMFile(book).upBookInfo() + } + + fun clear() { + mFile = null + } + } + + private var fileDescriptor: ParcelFileDescriptor? = null + private var mobiBook: MobiBook? = null + get() { + if (field == null || fileDescriptor == null) { + field = readMobi() + } + return field + } + + init { + upBookCover(true) + } + + private fun readMobi(): MobiBook? { + return kotlin.runCatching { + BookHelp.getBookPFD(book)?.let { + fileDescriptor = it + MobiReader().readMobi(it) + } + }.onFailure { + AppLog.put("读取Mobi文件失败\n${it.localizedMessage}", it) + it.printOnDebug() + }.getOrThrow() + } + + private fun getChapterList(): ArrayList { + return when (val book = mobiBook) { + is KF8Book -> getChapterListKF8(book) + is KF6Book -> getChapterListKF6(book) + else -> error("impossible condition") + } + } + + private fun getChapterListKF6(kF6Book: KF6Book): ArrayList { + val chapterList = arrayListOf() + val toc = kF6Book.toc + + if (kF6Book.sectionIdMap[0] == null) { + val section = kF6Book.sections.firstOrNull() + if (section != null) { + val chapter = BookChapter() + val content = kF6Book.getSectionText(section) + val soup = Jsoup.parse(content) + val title = soup.getElementsByTag("title").first()?.text() ?: "卷首" + chapter.bookUrl = book.bookUrl + chapter.title = title + chapter.url = "0:" + section.href + chapterList.add(chapter) + } + } + + fun append(ref: TOC) { + val chapter = BookChapter() + chapter.bookUrl = book.bookUrl + chapter.title = ref.label + chapter.url = "${chapterList.size}:${ref.href}" + chapter.isVolume = ref.subitems != null + val lastChapter = chapterList.lastOrNull() + if (lastChapter != null && + lastChapter.isVolume && + lastChapter.url.substringAfter(":") == chapter.url.substringAfter(":") + ) { + lastChapter.url = "skip:" + lastChapter.url + } + lastChapter?.putVariable("nextUrl", chapter.url) + chapterList.add(chapter) + ref.subitems?.forEach(::append) + } + + toc?.forEach(::append) + + return chapterList + } + + private fun getChapterListKF8(kf8Book: KF8Book): ArrayList { + val chapterList = arrayListOf() + val toc = kf8Book.toc + + if (kf8Book.sectionIdMap[0] == null) { + val section = kf8Book.sections.firstOrNull { it.href.isNotEmpty() } + if (section != null) { + val chapter = BookChapter() + val content = kf8Book.getSectionText(section) + val soup = Jsoup.parse(content) + val title = soup.getElementsByTag("title").first()?.text() ?: "卷首" + chapter.bookUrl = book.bookUrl + chapter.title = title + chapter.url = "0:" + section.href + chapterList.add(chapter) + } + } + + fun append(ref: TOC) { + val chapter = BookChapter() + chapter.bookUrl = book.bookUrl + chapter.title = ref.label + chapter.url = "${chapterList.size}:${ref.href}" + chapter.isVolume = ref.subitems != null + val lastChapter = chapterList.lastOrNull() + if (lastChapter != null && + lastChapter.isVolume && + lastChapter.url.substringAfter(":") == chapter.url.substringAfter(":") + ) { + lastChapter.url = "skip:" + lastChapter.url + } + lastChapter?.putVariable("nextUrl", chapter.url) + chapterList.add(chapter) + ref.subitems?.forEach(::append) + } + + toc?.forEach(::append) + + return chapterList + } + + private fun getContent(chapter: BookChapter): String? { + return when (val book = mobiBook) { + is KF8Book -> getContentKF8(book, chapter) + is KF6Book -> getContentKF6(book, chapter) + else -> error("impossible condition") + } + } + + private fun getContentKF6(kf6Book: KF6Book, chapter: BookChapter): String? { + if (chapter.isVolume && chapter.url.startsWith("skip:")) return "" + var section = kf6Book.getSectionByHref(chapter.url) ?: return null + val nextSectionHref = chapter.getVariable("nextUrl") + + val sb = StringBuilder() + sb.append(kf6Book.getSectionText(section)) + while (true) { + section = section.next ?: break + if (section.href == nextSectionHref) { + break + } + if (kf6Book.sectionIdMap[section.index] != null) { + break + } + sb.append(kf6Book.getSectionText(section)) + } + + val soup = Jsoup.parse(sb.toString()) + + soup.select("title").remove() + soup.select("[style*=display:none]").remove() + soup.select("img[recindex]").forEach { + val recindex = it.attr("recindex") + it.clearAttributes() + it.attr("src", "recindex:$recindex") + } + + return format(soup.outerHtml()) + } + + private fun getContentKF8(kf8Book: KF8Book, chapter: BookChapter): String? { + if (chapter.isVolume && chapter.url.startsWith("skip:")) return "" + var section = kf8Book.getSectionByHref(chapter.url) ?: return null + val nextSectionHref = chapter.getVariable("nextUrl") + val nextPos = kf8Book.parsePosURI(nextSectionHref) + + val sb = StringBuilder() + sb.append(kf8Book.getTextByHref(chapter.url, nextSectionHref)) + while (true) { + if (nextPos != null && section.frags.any { it.index == nextPos.fid }) { + break + } + section = section.next ?: break + if (!section.linear) { + continue + } + if (section.href == nextSectionHref) { + break + } + if (kf8Book.sectionIdMap[section.index] != null) { + break + } + sb.append(kf8Book.getSectionText(section)) + } + + val soup = Jsoup.parse(sb.toString()) + + soup.select("title").remove() + soup.select("[style*=display:none]").remove() + + return format(soup.outerHtml()) + } + + private fun format(html: String): String { + return HtmlFormatter.formatKeepImg(html) + .replace(xmlDeclarationRegex, "") + .replace(doctypeDeclarationRegex, "") + } + + private fun getImage(href: String): InputStream? { + return when (val book = mobiBook) { + is KF8Book -> getImageKF8(book, href) + is KF6Book -> getImageKF6(book, href) + else -> error("impossible condition") + } + } + + private fun getImageKF6(kf6Book: KF6Book, href: String): InputStream? { + return kf6Book.getResourceByHref(href)?.inputStream() + } + + private fun getImageKF8(kf8Book: KF8Book, href: String): InputStream? { + return kf8Book.getResourceByHref(href)?.inputStream() + } + + private fun upBookCover(fastCheck: Boolean = false) { + try { + mobiBook?.let { + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = LocalBook.getCoverPath(book) + } + if (fastCheck && File(book.coverUrl!!).exists()) { + return + } + it.getCover()?.let { bytes -> + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + val file = FileUtils.createFileIfNotExist(book.coverUrl!!) + FileOutputStream(file).use { out -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) + out.flush() + } + } + } + } catch (e: Exception) { + AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e) + e.printOnDebug() + } + } + + private fun upBookInfo() { + if (mobiBook == null) { + mFile = null + book.intro = "书籍导入异常" + } else { + upBookCover() + val metadata = mobiBook!!.metadata + book.name = metadata.title + if (book.name.isEmpty()) { + book.name = book.originName.replace("(?i)\\.(mobi|azw3)$".toRegex(), "") + } + if (metadata.author.isNotEmpty()) { + book.author = metadata.author.first() + } + if (metadata.description.isNotBlank()) { + book.intro = HtmlFormatter.format(metadata.description) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt b/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt new file mode 100755 index 000000000..c4c4ea772 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt @@ -0,0 +1,230 @@ +package io.legado.app.model.localBook + +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import androidx.core.graphics.createBitmap +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.getLocalUri +import io.legado.app.utils.BitmapUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.SystemUtils +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.printOnDebug +import splitties.init.appCtx +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import kotlin.math.ceil + + +class PdfFile(var book: Book) { + companion object : BaseLocalBookParse { + private var pFile: PdfFile? = null + + /** + * pdf分页尺寸 + */ + const val PAGE_SIZE = 10 + + @Synchronized + private fun getPFile(book: Book): PdfFile { + if (pFile == null || pFile?.book?.bookUrl != book.bookUrl) { + pFile = PdfFile(book) + return pFile!! + } + pFile?.book = book + return pFile!! + } + + @Synchronized + override fun upBookInfo(book: Book) { + getPFile(book).upBookInfo() + } + + @Synchronized + override fun getChapterList(book: Book): ArrayList { + return getPFile(book).getChapterList() + } + + @Synchronized + override fun getContent(book: Book, chapter: BookChapter): String? { + return getPFile(book).getContent(chapter) + } + + @Synchronized + override fun getImage(book: Book, href: String): InputStream? { + return getPFile(book).getImage(href) + } + + } + + /** + *持有引用,避免被回收 + */ + private var fileDescriptor: ParcelFileDescriptor? = null + private var pdfRenderer: PdfRenderer? = null + get() { + if (field != null && fileDescriptor != null) { + return field + } + field = readPdf() + return field + } + + init { + upBookCover(true) + } + + /** + * 读取PDF文件 + * + * @return + */ + private fun readPdf(): PdfRenderer? { + val uri = book.getLocalUri() + if (uri.isContentScheme()) { + fileDescriptor = appCtx.contentResolver.openFileDescriptor(uri, "r")?.also { + pdfRenderer = PdfRenderer(it) + } + } else { + fileDescriptor = + ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY) + ?.also { + pdfRenderer = PdfRenderer(it) + } + } + return pdfRenderer + } + + /** + * 关闭pdf文件 + * + */ + private fun closePdf() { + pdfRenderer?.close() + fileDescriptor?.close() + } + + + /** + * 渲染PDF页面 + * 根据index打开pdf页面,并渲染到Bitmap + * + * @param renderer + * @param index + * @return + */ + private fun openPdfPage(renderer: PdfRenderer, index: Int): Bitmap? { + if (index >= renderer.pageCount) { + return null + } + return renderer.openPage(index).use { page -> + createBitmap( + SystemUtils.screenWidthPx, + (SystemUtils.screenWidthPx.toDouble() * page.height / page.width).toInt() + ).apply { + this.eraseColor(Color.WHITE) + page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + } + } + + } + + private fun getContent(chapter: BookChapter): String? = + if (pdfRenderer == null) { + null + } else { + pdfRenderer?.let { renderer -> + + buildString { + val start = chapter.index * PAGE_SIZE + val end = ((chapter.index + 1) * PAGE_SIZE).coerceAtMost(renderer.pageCount) + (start until end).forEach { + append("") + .append('\n') + } + + } + + } + } + + + private fun getImage(href: String): InputStream? { + if (pdfRenderer == null) { + return null + } + return try { + val index = href.toInt() + val bitmap = openPdfPage(pdfRenderer!!, index) + if (bitmap != null) { + BitmapUtils.toInputStream(bitmap) + } else { + null + } + + } catch (_: Exception) { + return null + } + } + + private fun getChapterList(): ArrayList { + val chapterList = ArrayList() + + pdfRenderer?.let { renderer -> + if (renderer.pageCount > 0) { + val chapterCount = ceil((renderer.pageCount.toDouble() / PAGE_SIZE)).toInt() + (0 until chapterCount).forEach { + val chapter = BookChapter() + chapter.index = it + chapter.bookUrl = book.bookUrl + chapter.title = "分段_${it}" + chapter.url = "pdf_${it}" + chapterList.add(chapter) + } + } + } + return chapterList + } + + private fun upBookCover(fastCheck: Boolean = false) { + try { + pdfRenderer?.let { renderer -> + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = LocalBook.getCoverPath(book) + } + if (fastCheck && File(book.coverUrl!!).exists()) { + return + } + FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!)).use { out -> + openPdfPage(renderer, 0)?.compress(Bitmap.CompressFormat.JPEG, 90, out) + out.flush() + } + } + } catch (e: Exception) { + AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e) + e.printOnDebug() + } + } + + private fun upBookInfo() { + if (pdfRenderer == null) { + pFile = null + book.intro = "书籍导入异常" + } else { + upBookCover() + if (book.name.isEmpty()) { + book.name = book.originName.replace(".pdf", "") + } + } + + } + + protected fun finalize() { + closePdf() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/README.md b/app/src/main/java/io/legado/app/model/localBook/README.md new file mode 100755 index 000000000..e11cb2cde --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/README.md @@ -0,0 +1,8 @@ +# 书籍文件导入解析 + +* BaseLocalBookParse.kt 本地书籍解析接口 +* LocalBook.kt 导入解析总入口 +* TextFile.kt 解析txt +* EpubFile.kt 解析epub +* PdfFile.kt 解析pdf 纯图片形式 +* UmdFile.kt 解析umd \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/TextFile.kt b/app/src/main/java/io/legado/app/model/localBook/TextFile.kt new file mode 100755 index 000000000..7766e3734 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/TextFile.kt @@ -0,0 +1,479 @@ +package io.legado.app.model.localBook + +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.exception.EmptyFileException +import io.legado.app.help.DefaultData +import io.legado.app.help.book.isLocalModified +import io.legado.app.utils.EncodingDetect +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.StringUtils +import io.legado.app.utils.Utf8BomUtils +import java.io.FileNotFoundException +import java.nio.charset.Charset +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException +import kotlin.math.min + +class TextFile(private var book: Book) { + + @Suppress("ConstPropertyName") + companion object { + private val padRegex = "^[\\n\\s]+".toRegex() + private const val txtBufferSize = 8 * 1024 * 1024 + private var textFile: TextFile? = null + + @Synchronized + private fun getTextFile(book: Book): TextFile { + if (textFile == null || textFile?.book?.bookUrl != book.bookUrl || book.isLocalModified()) { + textFile = TextFile(book) + return textFile!! + } + textFile?.book = book + return textFile!! + } + + @Throws(FileNotFoundException::class) + fun getChapterList(book: Book): ArrayList { + return getTextFile(book).getChapterList() + } + + @Synchronized + @Throws(FileNotFoundException::class) + fun getContent(book: Book, bookChapter: BookChapter): String { + return getTextFile(book).getContent(bookChapter) + } + + fun clear() { + textFile = null + } + + } + + private val blank: Byte = 0x0a + + //默认从文件中获取数据的长度 + private val bufferSize = 512000 + + //没有标题的时候,每个章节的最大长度 + private val maxLengthWithNoToc = 10 * 1024 + + //使用正则划分目录,每个章节的最大允许长度 + private val maxLengthWithToc = 102400 + + private var charset: Charset = book.fileCharset() + + private var txtBuffer: ByteArray? = null + private var bufferStart = -1L + private var bufferEnd = -1L + + /** + * 获取目录 + */ + @Throws(FileNotFoundException::class, SecurityException::class, EmptyFileException::class) + fun getChapterList(): ArrayList { + val modified = book.isLocalModified() + if (book.charset == null || book.tocUrl.isBlank() || modified) { + LocalBook.getBookInputStream(book).use { bis -> + val buffer = ByteArray(bufferSize) + val length = bis.read(buffer) + if (length == -1) throw EmptyFileException("Unexpected Empty Txt File") + if (book.charset.isNullOrBlank() || modified) { + book.charset = EncodingDetect.getEncode(buffer.copyOf(length)) + } + charset = book.fileCharset() + if (book.tocUrl.isBlank() || modified) { + val blockContent = String(buffer, 0, length, charset) + book.tocUrl = getTocRule(blockContent)?.pattern() ?: "" + } + } + } + val (toc, wordCount) = analyze(book.tocUrl.toPattern(Pattern.MULTILINE)) + book.wordCount = StringUtils.wordCountFormat(wordCount) + toc.forEachIndexed { index, bookChapter -> + bookChapter.index = index + bookChapter.bookUrl = book.bookUrl + bookChapter.url = MD5Utils.md5Encode16(book.originName + index + bookChapter.title) + } + return toc + } + + fun getContent(chapter: BookChapter): String { + val start = chapter.start!! + val end = chapter.end!! + if (txtBuffer == null || start > bufferEnd || end < bufferStart) { + LocalBook.getBookInputStream(book).use { bis -> + bufferStart = txtBufferSize * (start / txtBufferSize) + txtBuffer = ByteArray(min(txtBufferSize, bis.available() - bufferStart.toInt())) + bufferEnd = bufferStart + txtBuffer!!.size + bis.skip(bufferStart) + bis.read(txtBuffer) + } + } + + val count = (end - start).toInt() + val buffer = ByteArray(count) + + @Suppress("ConvertTwoComparisonsToRangeCheck") + if (start < bufferEnd && end > bufferEnd || start < bufferStart && end > bufferStart) { + /** 章节内容在缓冲区交界处 */ + LocalBook.getBookInputStream(book).use { bis -> + bis.skip(start) + bis.read(buffer) + } + } else { + /** 章节内容在缓冲区内 */ + txtBuffer!!.copyInto( + buffer, + 0, + (start - bufferStart).toInt(), + (end - bufferStart).toInt() + ) + } + + return String(buffer, charset) + .substringAfter(chapter.title) + .replace(padRegex, "  ") + } + + /** + * 按规则解析目录 + */ + private fun analyze(pattern: Pattern?): Pair, Int> { + if (pattern == null || pattern.pattern().isNullOrEmpty()) { + return analyze() + } + val toc = arrayListOf() + var bookWordCount = 0 + LocalBook.getBookInputStream(book).use { bis -> + var blockContent: String + //加载章节 + var curOffset: Long = 0 + //读取的长度 + var length: Int + var lastChapterWordCount = 0 + val buffer = ByteArray(bufferSize) + var bufferStart = 3 + bis.read(buffer, 0, 3) + if (Utf8BomUtils.hasBom(buffer)) { + bufferStart = 0 + curOffset = 3 + } + //获取文件中的数据到buffer,直到没有数据为止 + while (bis.read( + buffer, bufferStart, bufferSize - bufferStart + ).also { length = it } > 0 + ) { + var end = bufferStart + length + if (end == bufferSize) { + for (i in bufferStart + length - 1 downTo 0) { + if (buffer[i] == blank) { + end = i + break + } + } + } + //将数据转换成String, 不能超过length + blockContent = String(buffer, 0, end, charset) + buffer.copyInto(buffer, 0, end, bufferStart + length) + bufferStart = bufferStart + length - end + length = end + //当前Block下使过的String的指针 + var seekPos = 0 + //进行正则匹配 + val matcher: Matcher = pattern.matcher(blockContent) + //如果存在相应章节 + while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置 + val chapterStart = matcher.start() + //获取章节内容 + val chapterContent = blockContent.substring(seekPos, chapterStart) + val chapterLength = chapterContent.toByteArray(charset).size.toLong() + val lastStart = toc.lastOrNull()?.start ?: curOffset + if (book.getSplitLongChapter() && curOffset + chapterLength - lastStart > maxLengthWithToc) { + toc.lastOrNull()?.let { + it.end = it.start + it.tag = null + } + //章节字数太多进行拆分 + val lastTitle = toc.lastOrNull()?.title + val lastTitleLength = lastTitle?.toByteArray(charset)?.size ?: 0 + val (chapters, wordCount) = analyze( + lastStart + lastTitleLength, curOffset + chapterLength + ) + lastTitle?.let { + chapters.forEachIndexed { index, bookChapter -> + bookChapter.title = "$lastTitle(${index + 1})" + } + } + toc.addAll(chapters) + bookWordCount += wordCount + //创建当前章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = curOffset + chapterLength + curChapter.end = curChapter.start + toc.add(curChapter) + lastChapterWordCount = 0 + } else if (seekPos == 0 && chapterStart != 0) { + /** + * 如果 seekPos == 0 && chapterStart != 0 表示当前block处前面有一段内容 + * 第一种情况一定是序章 第二种情况是上一个章节的内容 + */ + if (toc.isEmpty()) { //如果当前没有章节,那么就是序章 + //加入简介 + if (chapterContent.isNotBlank()) { + val qyChapter = BookChapter() + qyChapter.title = "前言" + qyChapter.start = curOffset + qyChapter.end = curOffset + chapterLength + qyChapter.wordCount = + StringUtils.wordCountFormat(chapterContent.length) + toc.add(qyChapter) + book.intro = if (chapterContent.length <= 500) { + chapterContent + } else { + chapterContent.substring(0, 500) + } + } + //创建当前章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = curOffset + chapterLength + curChapter.end = curChapter.start + toc.add(curChapter) + } else { //否则就block分割之后,上一个章节的剩余内容 + //获取上一章节 + val lastChapter = toc.last() + lastChapter.isVolume = + chapterContent.substringAfter(lastChapter.title).isBlank() + //将当前段落添加上一章去 + lastChapter.end = lastChapter.end!! + chapterLength + lastChapterWordCount += chapterContent.length + lastChapter.wordCount = + StringUtils.wordCountFormat(lastChapterWordCount) + //创建当前章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = lastChapter.end + curChapter.end = curChapter.start + toc.add(curChapter) + } + bookWordCount += chapterContent.length + lastChapterWordCount = 0 + } else { + if (toc.isNotEmpty()) { //获取章节内容 + //获取上一章节 + val lastChapter = toc.last() + lastChapter.isVolume = + chapterContent.substringAfter(lastChapter.title).isBlank() + lastChapter.end = + lastChapter.start!! + chapterLength + lastChapter.wordCount = + StringUtils.wordCountFormat(chapterContent.length) + //创建当前章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = lastChapter.end + curChapter.end = curChapter.start + toc.add(curChapter) + } else { //如果章节不存在则创建章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = curOffset + curChapter.end = curOffset + curChapter.wordCount = + StringUtils.wordCountFormat(chapterContent.length) + toc.add(curChapter) + } + bookWordCount += chapterContent.length + lastChapterWordCount = 0 + } + //设置指针偏移 + seekPos += chapterContent.length + } + val wordCount = blockContent.length - seekPos + bookWordCount += wordCount + lastChapterWordCount += wordCount + //block的偏移点 + curOffset += length.toLong() + //设置上一章的结尾 + toc.lastOrNull()?.let { + it.end = curOffset + it.wordCount = StringUtils.wordCountFormat(lastChapterWordCount) + } + } + toc.lastOrNull()?.let { chapter -> + //章节字数太多进行拆分 + if (book.getSplitLongChapter() && chapter.end!! - chapter.start!! > maxLengthWithToc) { + val end = chapter.end!! + chapter.end = chapter.start + chapter.tag = null + val lastTitle = chapter.title + val lastTitleLength = lastTitle.toByteArray(charset).size + val (chapters, _) = analyze( + chapter.start!! + lastTitleLength, end + ) + chapters.forEachIndexed { index, bookChapter -> + bookChapter.title = "$lastTitle(${index + 1})" + } + toc.addAll(chapters) + } + } + } + System.gc() + System.runFinalization() + return toc to bookWordCount + } + + /** + * 无规则拆分目录 + */ + private fun analyze( + fileStart: Long = 0L, fileEnd: Long = Long.MAX_VALUE + ): Pair, Int> { + val toc = arrayListOf() + var bookWordCount = 0 + LocalBook.getBookInputStream(book).use { bis -> + //block的个数 + var blockPos = 0 + //加载章节 + var curOffset: Long = 0 + var chapterPos = 0 + //读取的长度 + var length = 0 + var lastChapterWordCount = 0 + val buffer = ByteArray(bufferSize) + var bufferStart = 3 + if (fileStart == 0L) { + bis.read(buffer, 0, 3) + if (Utf8BomUtils.hasBom(buffer)) { + bufferStart = 0 + curOffset = 3 + } + } else { + bis.skip(fileStart) + curOffset = fileStart + bufferStart = 0 + } + //获取文件中的数据到buffer,直到没有数据为止 + while (fileEnd - curOffset - bufferStart > 0 && bis.read( + buffer, bufferStart, min( + (bufferSize - bufferStart).toLong(), fileEnd - curOffset - bufferStart + ).toInt() + ).also { length = it } > 0 + ) { + blockPos++ + //章节在buffer的偏移量 + var chapterOffset = 0 + //当前剩余可分配的长度 + length += bufferStart + var strLength = length + //分章的位置 + chapterPos = 0 + while (strLength > 0) { + chapterPos++ + //是否长度超过一章 + if (strLength > maxLengthWithNoToc) { //在buffer中一章的终止点 + var end = length + //寻找换行符作为终止点 + for (i in chapterOffset + maxLengthWithNoToc until length) { + if (buffer[i] == blank) { + end = i + break + } + } + val content = String(buffer, chapterOffset, end - chapterOffset, charset) + bookWordCount += content.length + lastChapterWordCount = content.length + val chapter = BookChapter() + chapter.title = "第${blockPos}章($chapterPos)" + chapter.start = toc.lastOrNull()?.end ?: curOffset + chapter.end = chapter.start!! + end - chapterOffset + chapter.wordCount = StringUtils.wordCountFormat(content.length) + toc.add(chapter) + //减去已经被分配的长度 + strLength -= (end - chapterOffset) + //设置偏移的位置 + chapterOffset = end + } else { + buffer.copyInto(buffer, 0, length - strLength, length) + length -= strLength + bufferStart = strLength + strLength = 0 + } + } + //block的偏移点 + curOffset += length.toLong() + } + //设置结尾章节 + val content = String(buffer, 0, bufferStart, charset) + bookWordCount += content.length + if (bufferStart > 100 || toc.isEmpty()) { + val chapter = BookChapter() + chapter.title = "第${blockPos}章(${chapterPos})" + chapter.start = toc.lastOrNull()?.end ?: curOffset + chapter.end = chapter.start!! + bufferStart + chapter.wordCount = StringUtils.wordCountFormat(content.length) + toc.add(chapter) + } else { + val wordCount = lastChapterWordCount + content.length + toc.lastOrNull()?.let { + it.end = it.end!! + bufferStart + it.wordCount = StringUtils.wordCountFormat(wordCount) + } + } + } + return toc to bookWordCount + } + + /** + * 获取合适的目录规则 + */ + private fun getTocRule(content: String): Pattern? { + val rules = getTocRules().reversed() + var maxNum = 1 + var tocPattern: Pattern? = null + for (tocRule in rules) { + val pattern = try { + tocRule.rule.toPattern(Pattern.MULTILINE) + } catch (e: PatternSyntaxException) { + AppLog.put("TXT目录规则正则语法错误:${tocRule.name}\n$e", e) + continue + } + val matcher = pattern.matcher(content) + var start = 0 + var num = 0 + while (matcher.find()) { + if (start == 0 || matcher.start() - start > 1000) { + num++ + start = matcher.end() + } + } + if (num >= maxNum) { + maxNum = num + tocPattern = pattern + } + } + return tocPattern + } + + /** + * 获取启用的目录规则 + */ + private fun getTocRules(): List { + var rules = appDb.txtTocRuleDao.enabled + if (appDb.txtTocRuleDao.count == 0) { + rules = DefaultData.txtTocRules.apply { + appDb.txtTocRuleDao.insert(*this.toTypedArray()) + }.filter { + it.enable + } + } + return rules + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/UmdFile.kt b/app/src/main/java/io/legado/app/model/localBook/UmdFile.kt new file mode 100755 index 000000000..c652ddc48 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/UmdFile.kt @@ -0,0 +1,123 @@ +package io.legado.app.model.localBook + +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.utils.DebugLog +import io.legado.app.utils.FileUtils +import io.legado.app.utils.printOnDebug +import me.ag2s.umdlib.domain.UmdBook +import me.ag2s.umdlib.umd.UmdReader +import java.io.File +import java.io.InputStream + +class UmdFile(var book: Book) { + companion object : BaseLocalBookParse { + private var uFile: UmdFile? = null + + @Synchronized + private fun getUFile(book: Book): UmdFile { + if (uFile == null || uFile?.book?.bookUrl != book.bookUrl) { + uFile = UmdFile(book) + return uFile!! + } + uFile?.book = book + return uFile!! + } + + @Synchronized + override fun getChapterList(book: Book): ArrayList { + return getUFile(book).getChapterList() + } + + @Synchronized + override fun getContent(book: Book, chapter: BookChapter): String? { + return getUFile(book).getContent(chapter) + } + + @Synchronized + override fun getImage( + book: Book, + href: String + ): InputStream? { + return getUFile(book).getImage(href) + } + + + @Synchronized + override fun upBookInfo(book: Book) { + return getUFile(book).upBookInfo() + } + } + + + private var umdBook: UmdBook? = null + get() { + if (field != null) { + return field + } + field = readUmd() + return field + } + + init { + upBookCover(true) + } + + private fun readUmd(): UmdBook? { + val input = LocalBook.getBookInputStream(book) + return UmdReader().read(input) + } + + private fun upBookCover(fastCheck: Boolean = false) { + try { + umdBook?.let { + if (book.coverUrl.isNullOrEmpty()) { + book.coverUrl = LocalBook.getCoverPath(book) + } + if (fastCheck && File(book.coverUrl!!).exists()) { + return + } + FileUtils.writeBytes(book.coverUrl!!, it.cover.coverData) + } + } catch (e: Exception) { + e.printOnDebug() + } + } + + private fun upBookInfo() { + if (umdBook == null) { + uFile = null + book.intro = "书籍导入异常" + } else { + upBookCover() + val hd = umdBook!!.header + book.name = hd.title + book.author = hd.author + book.kind = hd.bookType + } + } + + private fun getContent(chapter: BookChapter): String? { + return umdBook?.chapters?.getContentString(chapter.index) + } + + private fun getChapterList(): ArrayList { + val chapterList = ArrayList() + umdBook?.chapters?.titles?.forEachIndexed { index, _ -> + val title = umdBook!!.chapters.getTitle(index) + val chapter = BookChapter() + chapter.title = title + chapter.index = index + chapter.bookUrl = book.bookUrl + chapter.url = index.toString() + DebugLog.d(javaClass.name, chapter.url) + chapterList.add(chapter) + } + return chapterList + } + + private fun getImage(@Suppress("UNUSED_PARAMETER") href: String): InputStream? { + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/remote/RemoteBook.kt b/app/src/main/java/io/legado/app/model/remote/RemoteBook.kt new file mode 100755 index 000000000..f620b58d5 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/remote/RemoteBook.kt @@ -0,0 +1,31 @@ +package io.legado.app.model.remote + +import androidx.annotation.Keep +import io.legado.app.lib.webdav.WebDavFile +import io.legado.app.model.localBook.LocalBook + +@Keep +data class RemoteBook( + val filename: String, + val path: String, + val size: Long, + val lastModify: Long, + var contentType: String = "folder", + var isOnBookShelf: Boolean = false +) { + + val isDir get() = contentType == "folder" + + constructor(webDavFile: WebDavFile) : this( + webDavFile.displayName, + webDavFile.path, + webDavFile.size, + webDavFile.lastModify + ) { + if (!webDavFile.isDir) { + contentType = webDavFile.displayName.substringAfterLast(".") + isOnBookShelf = LocalBook.isOnBookShelf(webDavFile.displayName) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/remote/RemoteBookManager.kt b/app/src/main/java/io/legado/app/model/remote/RemoteBookManager.kt new file mode 100755 index 000000000..abfd68a74 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/remote/RemoteBookManager.kt @@ -0,0 +1,38 @@ +package io.legado.app.model.remote + +import android.net.Uri +import io.legado.app.data.entities.Book + +abstract class RemoteBookManager { + + /** + * 获取书籍列表 + */ + @Throws(Exception::class) + abstract suspend fun getRemoteBookList(path: String): MutableList + + /** + * 根据书籍地址获取书籍信息 + */ + @Throws(Exception::class) + abstract suspend fun getRemoteBook(path: String): RemoteBook? + + /** + * @return Uri:下载到本地的路径 + */ + @Throws(Exception::class) + abstract suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri + + /** + * 上传书籍 + */ + @Throws(Exception::class) + abstract suspend fun upload(book: Book) + + /** + * 删除书籍 + */ + @Throws(Exception::class) + abstract suspend fun delete(remoteBookUrl: String) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt b/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt new file mode 100755 index 000000000..31f8fa48c --- /dev/null +++ b/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt @@ -0,0 +1,90 @@ +package io.legado.app.model.remote + +import android.net.Uri +import io.legado.app.constant.AppPattern.archiveFileRegex +import io.legado.app.constant.AppPattern.bookFileRegex +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.update +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.webdav.Authorization +import io.legado.app.lib.webdav.WebDav +import io.legado.app.lib.webdav.WebDavFile +import io.legado.app.model.analyzeRule.CustomUrl +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.isContentScheme +import kotlinx.coroutines.runBlocking + +class RemoteBookWebDav( + val rootBookUrl: String, + val authorization: Authorization, + val serverID: Long? = null +) : RemoteBookManager() { + + init { + runBlocking { + WebDav(rootBookUrl, authorization).makeAsDir() + } + } + + + @Throws(Exception::class) + override suspend fun getRemoteBookList(path: String): MutableList { + if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") + val remoteBooks = mutableListOf() + //读取文件列表 + val remoteWebDavFileList: List = WebDav(path, authorization).listFiles() + //转化远程文件信息到本地对象 + remoteWebDavFileList.forEach { webDavFile -> + if (webDavFile.isDir + || bookFileRegex.matches(webDavFile.displayName) + || archiveFileRegex.matches(webDavFile.displayName) + ) { + //扩展名符合阅读的格式则认为是书籍 + remoteBooks.add(RemoteBook(webDavFile)) + } + } + return remoteBooks + } + + override suspend fun getRemoteBook(path: String): RemoteBook? { + if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") + val webDavFile = WebDav(path, authorization).getWebDavFile() + ?: return null + return RemoteBook(webDavFile) + } + + override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri { + AppConfig.defaultBookTreeUri + ?: throw NoStackTraceException("没有设置书籍保存位置!") + if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") + val webdav = WebDav(remoteBook.path, authorization) + return webdav.downloadInputStream().let { inputStream -> + LocalBook.saveBookFile(inputStream, remoteBook.filename) + } + } + + override suspend fun upload(book: Book) { + if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") + val localBookUri = Uri.parse(book.bookUrl) + val putUrl = "$rootBookUrl${book.originName}" + val webDav = WebDav(putUrl, authorization) + if (localBookUri.isContentScheme()) { + webDav.upload(localBookUri) + } else { + webDav.upload(localBookUri.path!!) + } + book.origin = BookType.webDavTag + CustomUrl(putUrl) + .putAttribute("serverID", serverID) + .toString() + book.update() + } + + override suspend fun delete(remoteBookUrl: String) { + if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") + WebDav(remoteBookUrl, authorization).delete() + } + +} diff --git a/app/src/main/java/io/legado/app/model/rss/Rss.kt b/app/src/main/java/io/legado/app/model/rss/Rss.kt new file mode 100755 index 000000000..519a1f0ee --- /dev/null +++ b/app/src/main/java/io/legado/app/model/rss/Rss.kt @@ -0,0 +1,103 @@ +package io.legado.app.model.rss + +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssSource +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.StrResponse +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.RuleData +import io.legado.app.utils.NetworkUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +@Suppress("MemberVisibilityCanBePrivate") +object Rss { + + fun getArticles( + scope: CoroutineScope, + sortName: String, + sortUrl: String, + rssSource: RssSource, + page: Int, + context: CoroutineContext = Dispatchers.IO + ): Coroutine, String?>> { + return Coroutine.async(scope, context) { + getArticlesAwait(sortName, sortUrl, rssSource, page) + } + } + + suspend fun getArticlesAwait( + sortName: String, + sortUrl: String, + rssSource: RssSource, + page: Int, + ): Pair, String?> { + val ruleData = RuleData() + val analyzeUrl = AnalyzeUrl( + sortUrl, + page = page, + source = rssSource, + ruleData = ruleData, + coroutineContext = coroutineContext, + hasLoginHeader = false + ) + val res = analyzeUrl.getStrResponseAwait() + checkRedirect(rssSource, res) + return RssParserByRule.parseXML(sortName, sortUrl, res.url, res.body, rssSource, ruleData) + } + + fun getContent( + scope: CoroutineScope, + rssArticle: RssArticle, + ruleContent: String, + rssSource: RssSource, + context: CoroutineContext = Dispatchers.IO + ): Coroutine { + return Coroutine.async(scope, context) { + getContentAwait(rssArticle, ruleContent, rssSource) + } + } + + suspend fun getContentAwait( + rssArticle: RssArticle, + ruleContent: String, + rssSource: RssSource, + ): String { + val analyzeUrl = AnalyzeUrl( + rssArticle.link, + baseUrl = rssArticle.origin, + source = rssSource, + ruleData = rssArticle, + coroutineContext = coroutineContext, + hasLoginHeader = false + ) + val res = analyzeUrl.getStrResponseAwait() + checkRedirect(rssSource, res) + Debug.log(rssSource.sourceUrl, "≡获取成功:${rssSource.sourceUrl}") + Debug.log(rssSource.sourceUrl, res.body ?: "", state = 20) + val analyzeRule = AnalyzeRule(rssArticle, rssSource) + analyzeRule.setContent(res.body) + .setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link)) + .setCoroutineContext(coroutineContext) + .setRedirectUrl(res.url) + return analyzeRule.getString(ruleContent) + } + + /** + * 检测重定向 + */ + private fun checkRedirect(rssSource: RssSource, response: StrResponse) { + response.raw.priorResponse?.let { + if (it.isRedirect) { + Debug.log(rssSource.sourceUrl, "≡检测到重定向(${it.code})") + Debug.log(rssSource.sourceUrl, "┌重定向后地址") + Debug.log(rssSource.sourceUrl, "└${response.url}") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt new file mode 100755 index 000000000..26fd751b0 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt @@ -0,0 +1,132 @@ +package io.legado.app.model.rss + +import androidx.annotation.Keep +import io.legado.app.R +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setRuleData +import io.legado.app.model.analyzeRule.RuleData +import io.legado.app.utils.NetworkUtils +import splitties.init.appCtx +import java.util.Locale +import kotlin.coroutines.coroutineContext + +@Keep +object RssParserByRule { + + @Throws(Exception::class) + suspend fun parseXML( + sortName: String, + sortUrl: String, + redirectUrl: String, + body: String?, + rssSource: RssSource, + ruleData: RuleData + ): Pair, String?> { + val sourceUrl = rssSource.sourceUrl + var nextUrl: String? = null + if (body.isNullOrBlank()) { + throw NoStackTraceException( + appCtx.getString(R.string.error_get_web_content, rssSource.sourceUrl) + ) + } + Debug.log(sourceUrl, "≡获取成功:$sourceUrl") + Debug.log(sourceUrl, body, state = 10) + var ruleArticles = rssSource.ruleArticles + if (ruleArticles.isNullOrBlank()) { + Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析") + return RssParserDefault.parseXML(sortName, body, sourceUrl) + } else { + val articleList = mutableListOf() + val analyzeRule = AnalyzeRule(ruleData, rssSource) + analyzeRule.setCoroutineContext(coroutineContext) + analyzeRule.setContent(body).setBaseUrl(sortUrl) + analyzeRule.setRedirectUrl(redirectUrl) + var reverse = false + if (ruleArticles.startsWith("-")) { + reverse = true + ruleArticles = ruleArticles.substring(1) + } + Debug.log(sourceUrl, "┌获取列表") + val collections = analyzeRule.getElements(ruleArticles) + Debug.log(sourceUrl, "└列表大小:${collections.size}") + if (!rssSource.ruleNextPage.isNullOrEmpty()) { + Debug.log(sourceUrl, "┌获取下一页链接") + if (rssSource.ruleNextPage!!.uppercase(Locale.getDefault()) == "PAGE") { + nextUrl = sortUrl + } else { + nextUrl = analyzeRule.getString(rssSource.ruleNextPage) + if (nextUrl.isNotEmpty()) { + nextUrl = NetworkUtils.getAbsoluteURL(sortUrl, nextUrl) + } + } + Debug.log(sourceUrl, "└$nextUrl") + } + val ruleTitle = analyzeRule.splitSourceRule(rssSource.ruleTitle) + val rulePubDate = analyzeRule.splitSourceRule(rssSource.rulePubDate) + val ruleDescription = analyzeRule.splitSourceRule(rssSource.ruleDescription) + val ruleImage = analyzeRule.splitSourceRule(rssSource.ruleImage) + val ruleLink = analyzeRule.splitSourceRule(rssSource.ruleLink) + val variable = ruleData.getVariable() + for ((index, item) in collections.withIndex()) { + getItem( + sourceUrl, item, analyzeRule, variable, index == 0, + ruleTitle, rulePubDate, ruleDescription, ruleImage, ruleLink + )?.let { + it.sort = sortName + it.origin = sourceUrl + articleList.add(it) + } + } + if (reverse) { + articleList.reverse() + } + return Pair(articleList, nextUrl) + } + } + + private fun getItem( + sourceUrl: String, + item: Any, + analyzeRule: AnalyzeRule, + variable: String?, + log: Boolean, + ruleTitle: List, + rulePubDate: List, + ruleDescription: List, + ruleImage: List, + ruleLink: List + ): RssArticle? { + val rssArticle = RssArticle(variable = variable) + analyzeRule.setRuleData(rssArticle) + analyzeRule.setContent(item) + Debug.log(sourceUrl, "┌获取标题", log) + rssArticle.title = analyzeRule.getString(ruleTitle) + Debug.log(sourceUrl, "└${rssArticle.title}", log) + Debug.log(sourceUrl, "┌获取时间", log) + rssArticle.pubDate = analyzeRule.getString(rulePubDate) + Debug.log(sourceUrl, "└${rssArticle.pubDate}", log) + Debug.log(sourceUrl, "┌获取描述", log) + if (ruleDescription.isEmpty()) { + rssArticle.description = null + Debug.log(sourceUrl, "└描述规则为空,将会解析内容页", log) + } else { + rssArticle.description = analyzeRule.getString(ruleDescription) + Debug.log(sourceUrl, "└${rssArticle.description}", log) + } + Debug.log(sourceUrl, "┌获取图片url", log) + rssArticle.image = analyzeRule.getString(ruleImage, isUrl = true) + Debug.log(sourceUrl, "└${rssArticle.image}", log) + Debug.log(sourceUrl, "┌获取文章链接", log) + rssArticle.link = NetworkUtils.getAbsoluteURL(sourceUrl, analyzeRule.getString(ruleLink)) + Debug.log(sourceUrl, "└${rssArticle.link}", log) + if (rssArticle.title.isBlank()) { + return null + } + return rssArticle + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt b/app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt new file mode 100755 index 000000000..6568283c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/rss/RssParserDefault.kt @@ -0,0 +1,150 @@ +package io.legado.app.model.rss + +import io.legado.app.data.entities.RssArticle +import io.legado.app.model.Debug +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.IOException +import java.io.StringReader + +@Suppress("unused") +object RssParserDefault { + + @Throws(XmlPullParserException::class, IOException::class) + fun parseXML( + sortName: String, + xml: String, + sourceUrl: String + ): Pair, String?> { + + val articleList = mutableListOf() + var currentArticle = RssArticle() + + val factory = XmlPullParserFactory.newInstance() + factory.isNamespaceAware = false + + val xmlPullParser = factory.newPullParser() + xmlPullParser.setInput(StringReader(xml)) + + // A flag just to be sure of the correct parsing + var insideItem = false + + var eventType = xmlPullParser.eventType + + // Start parsing the xml + loop@ while (eventType != XmlPullParser.END_DOCUMENT) { + + // Start parsing the item + if (eventType == XmlPullParser.START_TAG) { + when { + xmlPullParser.name.equals(RSS_ITEM, true) -> + insideItem = true + xmlPullParser.name.equals(RSS_ITEM_TITLE, true) -> + if (insideItem) currentArticle.title = xmlPullParser.nextText().trim() + xmlPullParser.name.equals(RSS_ITEM_LINK, true) -> + if (insideItem) currentArticle.link = xmlPullParser.nextText().trim() + xmlPullParser.name.equals(RSS_ITEM_THUMBNAIL, true) -> + if (insideItem) currentArticle.image = + xmlPullParser.getAttributeValue(null, RSS_ITEM_URL) + xmlPullParser.name.equals(RSS_ITEM_ENCLOSURE, true) -> + if (insideItem) { + val type = + xmlPullParser.getAttributeValue(null, RSS_ITEM_TYPE) + if (type != null && type.contains("image/")) { + currentArticle.image = + xmlPullParser.getAttributeValue(null, RSS_ITEM_URL) + } + } + xmlPullParser.name + .equals(RSS_ITEM_DESCRIPTION, true) -> + if (insideItem) { + val description = xmlPullParser.nextText() + currentArticle.description = description.trim() + if (currentArticle.image == null) { + currentArticle.image = getImageUrl(description) + } + } + xmlPullParser.name.equals(RSS_ITEM_CONTENT, true) -> + if (insideItem) { + val content = xmlPullParser.nextText().trim() + currentArticle.content = content + if (currentArticle.image == null) { + currentArticle.image = getImageUrl(content) + } + } + xmlPullParser.name + .equals(RSS_ITEM_PUB_DATE, true) -> + if (insideItem) { + val nextTokenType = xmlPullParser.next() + if (nextTokenType == XmlPullParser.TEXT) { + currentArticle.pubDate = xmlPullParser.text.trim() + } + // Skip to be able to find date inside 'tag' tag + continue@loop + } + xmlPullParser.name.equals(RSS_ITEM_TIME, true) -> + if (insideItem) currentArticle.pubDate = xmlPullParser.nextText() + } + } else if (eventType == XmlPullParser.END_TAG + && xmlPullParser.name.equals("item", true) + ) { + // The item is correctly parsed + insideItem = false + currentArticle.origin = sourceUrl + currentArticle.sort = sortName + articleList.add(currentArticle) + currentArticle = RssArticle() + } + eventType = xmlPullParser.next() + } + articleList.firstOrNull()?.let { + Debug.log(sourceUrl, "┌获取标题") + Debug.log(sourceUrl, "└${it.title}") + Debug.log(sourceUrl, "┌获取时间") + Debug.log(sourceUrl, "└${it.pubDate}") + Debug.log(sourceUrl, "┌获取描述") + Debug.log(sourceUrl, "└${it.description}") + Debug.log(sourceUrl, "┌获取图片url") + Debug.log(sourceUrl, "└${it.image}") + Debug.log(sourceUrl, "┌获取文章链接") + Debug.log(sourceUrl, "└${it.link}") + } + return Pair(articleList, null) + } + + /** + * Finds the first img tag and get the src as featured image + * + * @param input The content in which to search for the tag + * @return The url, if there is one + */ + private fun getImageUrl(input: String): String? { + + var url: String? = null + val patternImg = "(]*>)".toPattern() + val matcherImg = patternImg.matcher(input) + if (matcherImg.find()) { + val imgTag = matcherImg.group(1) + val patternLink = "src\\s*=\\s*\"([^\"]+)\"".toPattern() + val matcherLink = patternLink.matcher(imgTag!!) + if (matcherLink.find()) { + url = matcherLink.group(1)!!.trim() + } + } + return url + } + + private const val RSS_ITEM = "item" + private const val RSS_ITEM_TITLE = "title" + private const val RSS_ITEM_LINK = "link" + private const val RSS_ITEM_CATEGORY = "category" + private const val RSS_ITEM_THUMBNAIL = "media:thumbnail" + private const val RSS_ITEM_ENCLOSURE = "enclosure" + private const val RSS_ITEM_DESCRIPTION = "description" + private const val RSS_ITEM_CONTENT = "content:encoded" + private const val RSS_ITEM_PUB_DATE = "pubDate" + private const val RSS_ITEM_TIME = "time" + private const val RSS_ITEM_URL = "url" + private const val RSS_ITEM_TYPE = "type" +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt new file mode 100755 index 000000000..01ad6e9ca --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt @@ -0,0 +1,288 @@ +package io.legado.app.model.webBook + +import android.text.TextUtils +import com.script.ScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.rule.TocRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.exception.TocEmptyException +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.config.AppConfig +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.isTrue +import io.legado.app.utils.mapAsync +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.flow +import org.mozilla.javascript.Context +import splitties.init.appCtx +import kotlin.coroutines.coroutineContext + +/** + * 获取目录 + */ +object BookChapterList { + + suspend fun analyzeChapterList( + bookSource: BookSource, + book: Book, + baseUrl: String, + redirectUrl: String, + body: String? + ): List { + body ?: throw NoStackTraceException( + appCtx.getString(R.string.error_get_web_content, baseUrl) + ) + val chapterList = ArrayList() + Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") + Debug.log(bookSource.bookSourceUrl, body, state = 30) + val tocRule = bookSource.getTocRule() + val nextUrlList = arrayListOf(redirectUrl) + var reverse = false + var listRule = tocRule.chapterList ?: "" + if (listRule.startsWith("-")) { + reverse = true + listRule = listRule.substring(1) + } + if (listRule.startsWith("+")) { + listRule = listRule.substring(1) + } + var chapterData = + analyzeChapterList( + book, baseUrl, redirectUrl, body, + tocRule, listRule, bookSource, log = true + ) + chapterList.addAll(chapterData.first) + when (chapterData.second.size) { + 0 -> Unit + 1 -> { + var nextUrl = chapterData.second[0] + while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { + nextUrlList.add(nextUrl) + val analyzeUrl = AnalyzeUrl( + mUrl = nextUrl, + source = bookSource, + ruleData = book, + coroutineContext = coroutineContext + ) + val res = analyzeUrl.getStrResponseAwait() //控制并发访问 + res.body?.let { nextBody -> + chapterData = analyzeChapterList( + book, nextUrl, nextUrl, + nextBody, tocRule, listRule, bookSource + ) + nextUrl = chapterData.second.firstOrNull() ?: "" + chapterList.addAll(chapterData.first) + } + } + Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") + } + + else -> { + Debug.log( + bookSource.bookSourceUrl, + "◇并发解析目录,总页数:${chapterData.second.size}" + ) + flow { + for (urlStr in chapterData.second) { + emit(urlStr) + } + }.mapAsync(AppConfig.threadCount) { urlStr -> + val analyzeUrl = AnalyzeUrl( + mUrl = urlStr, + source = bookSource, + ruleData = book, + coroutineContext = coroutineContext + ) + val res = analyzeUrl.getStrResponseAwait() //控制并发访问 + analyzeChapterList( + book, urlStr, res.url, + res.body!!, tocRule, listRule, bookSource, false + ).first + }.collect { + chapterList.addAll(it) + } + } + } + if (chapterList.isEmpty()) { + throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty)) + } + if (!reverse) { + chapterList.reverse() + } + coroutineContext.ensureActive() + //去重 + val lh = LinkedHashSet(chapterList) + val list = ArrayList(lh) + if (!book.getReverseToc()) { + list.reverse() + } + Debug.log(book.origin, "◇目录总数:${list.size}") + coroutineContext.ensureActive() + list.forEachIndexed { index, bookChapter -> + bookChapter.index = index + } + val formatJs = tocRule.formatJs + if (!formatJs.isNullOrBlank()) { + Context.enter().use { + val bindings = ScriptBindings() + bindings["gInt"] = 0 + list.forEachIndexed { index, bookChapter -> + bindings["index"] = index + 1 + bindings["chapter"] = bookChapter + bindings["title"] = bookChapter.title + RhinoScriptEngine.runCatching { + eval(formatJs, bindings)?.toString()?.let { + bookChapter.title = it + } + }.onFailure { + Debug.log(book.origin, "格式化标题出错, ${it.localizedMessage}") + } + } + } + } + val replaceRules = ContentProcessor.get(book).getTitleReplaceRules() + book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() } + .getDisplayTitle(replaceRules, book.getUseReplaceRule()) + if (book.totalChapterNum < list.size) { + book.lastCheckCount = list.size - book.totalChapterNum + book.latestChapterTime = System.currentTimeMillis() + } + book.lastCheckTime = System.currentTimeMillis() + book.totalChapterNum = list.size + book.latestChapterTitle = + list.getOrElse(book.simulatedTotalChapterNum() - 1) { list.last() } + .getDisplayTitle(replaceRules, book.getUseReplaceRule()) + coroutineContext.ensureActive() + getWordCount(list, book) + return list + } + + private suspend fun analyzeChapterList( + book: Book, + baseUrl: String, + redirectUrl: String, + body: String, + tocRule: TocRule, + listRule: String, + bookSource: BookSource, + getNextUrl: Boolean = true, + log: Boolean = false + ): Pair, List> { + val analyzeRule = AnalyzeRule(book, bookSource) + analyzeRule.setContent(body).setBaseUrl(baseUrl) + analyzeRule.setRedirectUrl(redirectUrl) + analyzeRule.setCoroutineContext(coroutineContext) + //获取目录列表 + val chapterList = arrayListOf() + Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) + val elements = analyzeRule.getElements(listRule) + Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) + //获取下一页链接 + val nextUrlList = arrayListOf() + val nextTocRule = tocRule.nextTocUrl + if (getNextUrl && !nextTocRule.isNullOrEmpty()) { + Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log) + analyzeRule.getStringList(nextTocRule, isUrl = true)?.let { + for (item in it) { + if (item != redirectUrl) { + nextUrlList.add(item) + } + } + } + Debug.log( + bookSource.bookSourceUrl, + "└" + TextUtils.join(",\n", nextUrlList), + log + ) + } + coroutineContext.ensureActive() + if (elements.isNotEmpty()) { + Debug.log(bookSource.bookSourceUrl, "┌解析目录列表", log) + val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) + val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) + val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) + val payRule = analyzeRule.splitSourceRule(tocRule.isPay) + val upTimeRule = analyzeRule.splitSourceRule(tocRule.updateTime) + val isVolumeRule = analyzeRule.splitSourceRule(tocRule.isVolume) + elements.forEachIndexed { index, item -> + coroutineContext.ensureActive() + analyzeRule.setContent(item) + val bookChapter = BookChapter(bookUrl = book.bookUrl, baseUrl = redirectUrl) + analyzeRule.setChapter(bookChapter) + bookChapter.title = analyzeRule.getString(nameRule) + bookChapter.url = analyzeRule.getString(urlRule) + bookChapter.tag = analyzeRule.getString(upTimeRule) + val isVolume = analyzeRule.getString(isVolumeRule) + bookChapter.isVolume = false + if (isVolume.isTrue()) { + bookChapter.isVolume = true + } + if (bookChapter.url.isEmpty()) { + if (bookChapter.isVolume) { + bookChapter.url = bookChapter.title + index + Debug.log( + bookSource.bookSourceUrl, + "⇒一级目录${index}未获取到url,使用标题替代" + ) + } else { + bookChapter.url = baseUrl + Debug.log( + bookSource.bookSourceUrl, + "⇒目录${index}未获取到url,使用baseUrl替代" + ) + } + } + if (bookChapter.title.isNotEmpty()) { + val isVip = analyzeRule.getString(vipRule) + val isPay = analyzeRule.getString(payRule) + if (isVip.isTrue()) { + bookChapter.isVip = true + } + if (isPay.isTrue()) { + bookChapter.isPay = true + } + chapterList.add(bookChapter) + } + } + Debug.log(bookSource.bookSourceUrl, "└目录列表解析完成", log) + if (chapterList.isEmpty()) { + Debug.log(bookSource.bookSourceUrl, "◇章节列表为空", log) + } else { + Debug.log(bookSource.bookSourceUrl, "≡首章信息", log) + Debug.log(bookSource.bookSourceUrl, "◇章节名称:${chapterList[0].title}", log) + Debug.log(bookSource.bookSourceUrl, "◇章节链接:${chapterList[0].url}", log) + Debug.log(bookSource.bookSourceUrl, "◇章节信息:${chapterList[0].tag}", log) + Debug.log(bookSource.bookSourceUrl, "◇是否VIP:${chapterList[0].isVip}", log) + Debug.log(bookSource.bookSourceUrl, "◇是否购买:${chapterList[0].isPay}", log) + } + } + return Pair(chapterList, nextUrlList) + } + + private fun getWordCount(list: ArrayList, book: Book) { + if (!AppConfig.tocCountWords) { + return + } + val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl) + if (chapterList.isNotEmpty()) { + val map = chapterList.associateBy({ it.getFileName() }, { it.wordCount }) + for (bookChapter in list) { + val wordCount = map[bookChapter.getFileName()] + if (wordCount != null) { + bookChapter.wordCount = wordCount + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt new file mode 100755 index 000000000..6208254bf --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt @@ -0,0 +1,197 @@ +package io.legado.app.model.webBook + +import io.legado.app.R +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.rule.ContentRule +import io.legado.app.exception.ContentEmptyException +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.mapAsync +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.flow +import org.apache.commons.text.StringEscapeUtils +import splitties.init.appCtx +import kotlin.coroutines.coroutineContext + +/** + * 获取正文 + */ +object BookContent { + + @Throws(Exception::class) + suspend fun analyzeContent( + bookSource: BookSource, + book: Book, + bookChapter: BookChapter, + baseUrl: String, + redirectUrl: String, + body: String?, + nextChapterUrl: String?, + needSave: Boolean = true + ): String { + body ?: throw NoStackTraceException( + appCtx.getString(R.string.error_get_web_content, baseUrl) + ) + Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") + Debug.log(bookSource.bookSourceUrl, body, state = 40) + val mNextChapterUrl = if (nextChapterUrl.isNullOrEmpty()) { + appDb.bookChapterDao.getChapter(book.bookUrl, bookChapter.index + 1)?.url + ?: appDb.bookChapterDao.getChapter(book.bookUrl, 0)?.url + } else { + nextChapterUrl + } + val contentList = arrayListOf() + val nextUrlList = arrayListOf(redirectUrl) + val contentRule = bookSource.getContentRule() + val analyzeRule = AnalyzeRule(book, bookSource) + analyzeRule.setContent(body, baseUrl) + analyzeRule.setRedirectUrl(redirectUrl) + analyzeRule.setCoroutineContext(coroutineContext) + analyzeRule.setChapter(bookChapter) + analyzeRule.setNextChapterUrl(mNextChapterUrl) + coroutineContext.ensureActive() + val titleRule = contentRule.title + if (!titleRule.isNullOrBlank()) { + val title = analyzeRule.runCatching { + getString(titleRule) + }.onFailure { + Debug.log(bookSource.bookSourceUrl, "获取标题出错, ${it.localizedMessage}") + }.getOrNull() + if (!title.isNullOrBlank()) { + bookChapter.title = title + bookChapter.titleMD5 = null + appDb.bookChapterDao.update(bookChapter) + } + } + var contentData = analyzeContent( + book, baseUrl, redirectUrl, body, contentRule, bookChapter, bookSource, mNextChapterUrl + ) + contentList.add(contentData.first) + if (contentData.second.size == 1) { + var nextUrl = contentData.second[0] + while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { + if (!mNextChapterUrl.isNullOrEmpty() + && NetworkUtils.getAbsoluteURL(redirectUrl, nextUrl) + == NetworkUtils.getAbsoluteURL(redirectUrl, mNextChapterUrl) + ) break + nextUrlList.add(nextUrl) + coroutineContext.ensureActive() + val analyzeUrl = AnalyzeUrl( + mUrl = nextUrl, + source = bookSource, + ruleData = book, + coroutineContext = coroutineContext + ) + val res = analyzeUrl.getStrResponseAwait() //控制并发访问 + res.body?.let { nextBody -> + contentData = analyzeContent( + book, nextUrl, res.url, nextBody, contentRule, + bookChapter, bookSource, mNextChapterUrl, + printLog = false + ) + nextUrl = + if (contentData.second.isNotEmpty()) contentData.second[0] else "" + contentList.add(contentData.first) + Debug.log(bookSource.bookSourceUrl, "第${contentList.size}页完成") + } + } + Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") + } else if (contentData.second.size > 1) { + Debug.log(bookSource.bookSourceUrl, "◇并发解析正文,总页数:${contentData.second.size}") + flow { + for (urlStr in contentData.second) { + emit(urlStr) + } + }.mapAsync(AppConfig.threadCount) { urlStr -> + val analyzeUrl = AnalyzeUrl( + mUrl = urlStr, + source = bookSource, + ruleData = book, + coroutineContext = coroutineContext + ) + val res = analyzeUrl.getStrResponseAwait() //控制并发访问 + analyzeContent( + book, urlStr, res.url, res.body!!, contentRule, + bookChapter, bookSource, mNextChapterUrl, + getNextPageUrl = false, + printLog = false + ).first + }.collect { + coroutineContext.ensureActive() + contentList.add(it) + } + } + var contentStr = contentList.joinToString("\n") + //全文替换 + val replaceRegex = contentRule.replaceRegex + if (!replaceRegex.isNullOrEmpty()) { + contentStr = contentStr.split(AppPattern.LFRegex).joinToString("\n") { it.trim() } + contentStr = analyzeRule.getString(replaceRegex, contentStr) + contentStr = contentStr.split(AppPattern.LFRegex).joinToString("\n") { "  $it" } + } + Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") + Debug.log(bookSource.bookSourceUrl, "└${bookChapter.title}") + Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") + Debug.log(bookSource.bookSourceUrl, "└\n$contentStr") + if (!bookChapter.isVolume && contentStr.isBlank()) { + throw ContentEmptyException("内容为空") + } + if (needSave) { + BookHelp.saveContent(bookSource, book, bookChapter, contentStr) + } + return contentStr + } + + @Throws(Exception::class) + private suspend fun analyzeContent( + book: Book, + baseUrl: String, + redirectUrl: String, + body: String, + contentRule: ContentRule, + chapter: BookChapter, + bookSource: BookSource, + nextChapterUrl: String?, + getNextPageUrl: Boolean = true, + printLog: Boolean = true + ): Pair> { + val analyzeRule = AnalyzeRule(book, bookSource) + analyzeRule.setContent(body, baseUrl) + analyzeRule.setCoroutineContext(coroutineContext) + val rUrl = analyzeRule.setRedirectUrl(redirectUrl) + analyzeRule.setNextChapterUrl(nextChapterUrl) + val nextUrlList = arrayListOf() + analyzeRule.setChapter(chapter) + //获取正文 + var content = analyzeRule.getString(contentRule.content, unescape = false) + content = HtmlFormatter.formatKeepImg(content, rUrl) + if (content.indexOf('&') > -1) { + content = StringEscapeUtils.unescapeHtml4(content) + } + //获取下一页链接 + if (getNextPageUrl) { + val nextUrlRule = contentRule.nextContentUrl + if (!nextUrlRule.isNullOrEmpty()) { + Debug.log(bookSource.bookSourceUrl, "┌获取正文下一页链接", printLog) + analyzeRule.getStringList(nextUrlRule, isUrl = true)?.let { + nextUrlList.addAll(it) + } + Debug.log(bookSource.bookSourceUrl, "└" + nextUrlList.joinToString(","), printLog) + } + } + return Pair(content, nextUrlList) + } +} diff --git a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt new file mode 100755 index 000000000..f90d5fab0 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt @@ -0,0 +1,171 @@ +package io.legado.app.model.webBook + +import android.text.TextUtils +import io.legado.app.R +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isWebFile +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.utils.DebugLog +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.StringUtils.wordCountFormat +import kotlinx.coroutines.ensureActive +import splitties.init.appCtx +import kotlin.coroutines.coroutineContext + + +/** + * 获取详情 + */ +object BookInfo { + + @Throws(Exception::class) + suspend fun analyzeBookInfo( + bookSource: BookSource, + book: Book, + baseUrl: String, + redirectUrl: String, + body: String?, + canReName: Boolean, + ) { + body ?: throw NoStackTraceException( + appCtx.getString(R.string.error_get_web_content, baseUrl) + ) + Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") + Debug.log(bookSource.bookSourceUrl, body, state = 20) + val analyzeRule = AnalyzeRule(book, bookSource) + analyzeRule.setContent(body).setBaseUrl(baseUrl) + analyzeRule.setRedirectUrl(redirectUrl) + analyzeRule.setCoroutineContext(coroutineContext) + analyzeBookInfo(book, body, analyzeRule, bookSource, baseUrl, redirectUrl, canReName) + } + + suspend fun analyzeBookInfo( + book: Book, + body: String, + analyzeRule: AnalyzeRule, + bookSource: BookSource, + baseUrl: String, + redirectUrl: String, + canReName: Boolean, + ) { + val infoRule = bookSource.getBookInfoRule() + infoRule.init?.let { + if (it.isNotBlank()) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "≡执行详情页初始化规则") + analyzeRule.setContent(analyzeRule.getElement(it)) + } + } + val mCanReName = canReName && !infoRule.canReName.isNullOrBlank() + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取书名") + BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let { + if (it.isNotEmpty() && (mCanReName || book.name.isEmpty())) { + book.name = it + } + Debug.log(bookSource.bookSourceUrl, "└${it}") + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取作者") + BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let { + if (it.isNotEmpty() && (mCanReName || book.author.isEmpty())) { + book.author = it + } + Debug.log(bookSource.bookSourceUrl, "└${it}") + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取分类") + try { + analyzeRule.getStringList(infoRule.kind) + ?.joinToString(",") + ?.let { + if (it.isNotEmpty()) book.kind = it + Debug.log(bookSource.bookSourceUrl, "└${it}") + } ?: Debug.log(bookSource.bookSourceUrl, "└") + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") + DebugLog.e("获取分类出错", e) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取字数") + try { + wordCountFormat(analyzeRule.getString(infoRule.wordCount)).let { + if (it.isNotEmpty()) book.wordCount = it + Debug.log(bookSource.bookSourceUrl, "└${it}") + } + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") + DebugLog.e("获取字数出错", e) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取最新章节") + try { + analyzeRule.getString(infoRule.lastChapter).let { + if (it.isNotEmpty()) book.latestChapterTitle = it + Debug.log(bookSource.bookSourceUrl, "└${it}") + } + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") + DebugLog.e("获取最新章节出错", e) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取简介") + try { + HtmlFormatter.format(analyzeRule.getString(infoRule.intro)).let { + if (it.isNotEmpty()) book.intro = it + Debug.log(bookSource.bookSourceUrl, "└${it}") + } + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") + DebugLog.e("获取简介出错", e) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取封面链接") + try { + analyzeRule.getString(infoRule.coverUrl).let { + if (it.isNotEmpty()) { + book.coverUrl = + NetworkUtils.getAbsoluteURL(redirectUrl, it) + } + Debug.log(bookSource.bookSourceUrl, "└${it}") + } + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") + DebugLog.e("获取封面出错", e) + } + coroutineContext.ensureActive() + if (!book.isWebFile) { + Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") + book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true) + if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl + if (book.tocUrl == baseUrl) { + book.tocHtml = body + } + Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}") + } else { + Debug.log(bookSource.bookSourceUrl, "┌获取文件下载链接") + book.downloadUrls = analyzeRule.getStringList(infoRule.downloadUrls, isUrl = true) + if (book.downloadUrls.isNullOrEmpty()) { + Debug.log(bookSource.bookSourceUrl, "└") + throw NoStackTraceException("下载链接为空") + } else { + Debug.log( + bookSource.bookSourceUrl, + "└" + TextUtils.join(",\n", book.downloadUrls!!) + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/BookList.kt b/app/src/main/java/io/legado/app/model/webBook/BookList.kt new file mode 100755 index 000000000..10898874f --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/BookList.kt @@ -0,0 +1,308 @@ +package io.legado.app.model.webBook + +import io.legado.app.R +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.rule.BookListRule +import io.legado.app.data.entities.rule.ExploreKind +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.source.exploreKindsJson +import io.legado.app.help.source.getBookType +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setRuleData +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.RuleData +import io.legado.app.utils.GSON +import io.legado.app.utils.GSONStrict +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.StringUtils.wordCountFormat +import io.legado.app.utils.fromJsonArray +import kotlinx.coroutines.ensureActive +import splitties.init.appCtx +import kotlin.coroutines.coroutineContext + +/** + * 获取书籍列表 + */ +object BookList { + + @Throws(Exception::class) + suspend fun analyzeBookList( + bookSource: BookSource, + ruleData: RuleData, + analyzeUrl: AnalyzeUrl, + baseUrl: String, + body: String?, + isSearch: Boolean = true, + isRedirect: Boolean = false, + filter: ((name: String, author: String) -> Boolean)? = null, + shouldBreak: ((size: Int) -> Boolean)? = null + ): ArrayList { + body ?: throw NoStackTraceException( + appCtx.getString( + R.string.error_get_web_content, + analyzeUrl.ruleUrl + ) + ) + val bookList = ArrayList() + Debug.log(bookSource.bookSourceUrl, "≡获取成功:${analyzeUrl.ruleUrl}") + Debug.log(bookSource.bookSourceUrl, body, state = 10) + val analyzeRule = AnalyzeRule(ruleData, bookSource) + analyzeRule.setContent(body).setBaseUrl(baseUrl) + analyzeRule.setRedirectUrl(baseUrl) + analyzeRule.setCoroutineContext(coroutineContext) + if (!isSearch) { + checkExploreJson(bookSource) + } + if (isSearch) bookSource.bookUrlPattern?.let { + coroutineContext.ensureActive() + if (baseUrl.matches(it.toRegex())) { + Debug.log(bookSource.bookSourceUrl, "≡链接为详情页") + getInfoItem( + bookSource, + analyzeRule, + analyzeUrl, + body, + baseUrl, + ruleData.getVariable(), + isRedirect, + filter + )?.let { searchBook -> + searchBook.infoHtml = body + bookList.add(searchBook) + } + return bookList + } + } + val collections: List + var reverse = false + val bookListRule: BookListRule = when { + isSearch -> bookSource.getSearchRule() + bookSource.getExploreRule().bookList.isNullOrBlank() -> bookSource.getSearchRule() + else -> bookSource.getExploreRule() + } + var ruleList: String = bookListRule.bookList ?: "" + if (ruleList.startsWith("-")) { + reverse = true + ruleList = ruleList.substring(1) + } + if (ruleList.startsWith("+")) { + ruleList = ruleList.substring(1) + } + Debug.log(bookSource.bookSourceUrl, "┌获取书籍列表") + collections = analyzeRule.getElements(ruleList) + coroutineContext.ensureActive() + if (collections.isEmpty() && bookSource.bookUrlPattern.isNullOrEmpty()) { + Debug.log(bookSource.bookSourceUrl, "└列表为空,按详情页解析") + getInfoItem( + bookSource, analyzeRule, analyzeUrl, body, baseUrl, ruleData.getVariable(), + isRedirect, filter + )?.let { searchBook -> + searchBook.infoHtml = body + bookList.add(searchBook) + } + } else { + val ruleName = analyzeRule.splitSourceRule(bookListRule.name) + val ruleBookUrl = analyzeRule.splitSourceRule(bookListRule.bookUrl) + val ruleAuthor = analyzeRule.splitSourceRule(bookListRule.author) + val ruleCoverUrl = analyzeRule.splitSourceRule(bookListRule.coverUrl) + val ruleIntro = analyzeRule.splitSourceRule(bookListRule.intro) + val ruleKind = analyzeRule.splitSourceRule(bookListRule.kind) + val ruleLastChapter = analyzeRule.splitSourceRule(bookListRule.lastChapter) + val ruleWordCount = analyzeRule.splitSourceRule(bookListRule.wordCount) + Debug.log(bookSource.bookSourceUrl, "└列表大小:${collections.size}") + for ((index, item) in collections.withIndex()) { + getSearchItem( + bookSource, analyzeRule, item, baseUrl, ruleData.getVariable(), + index == 0, + filter, + ruleName = ruleName, + ruleBookUrl = ruleBookUrl, + ruleAuthor = ruleAuthor, + ruleCoverUrl = ruleCoverUrl, + ruleIntro = ruleIntro, + ruleKind = ruleKind, + ruleLastChapter = ruleLastChapter, + ruleWordCount = ruleWordCount + )?.let { searchBook -> + if (baseUrl == searchBook.bookUrl) { + searchBook.infoHtml = body + } + bookList.add(searchBook) + } + if (shouldBreak?.invoke(bookList.size) == true) { + break + } + } + val lh = LinkedHashSet(bookList) + bookList.clear() + bookList.addAll(lh) + if (reverse) { + bookList.reverse() + } + } + Debug.log(bookSource.bookSourceUrl, "◇书籍总数:${bookList.size}") + return bookList + } + + @Throws(Exception::class) + private suspend fun getInfoItem( + bookSource: BookSource, + analyzeRule: AnalyzeRule, + analyzeUrl: AnalyzeUrl, + body: String, + baseUrl: String, + variable: String?, + isRedirect: Boolean, + filter: ((name: String, author: String) -> Boolean)? + ): SearchBook? { + val book = Book(variable = variable) + book.bookUrl = if (isRedirect) { + baseUrl + } else { + NetworkUtils.getAbsoluteURL(analyzeUrl.url, analyzeUrl.ruleUrl) + } + book.origin = bookSource.bookSourceUrl + book.originName = bookSource.bookSourceName + book.originOrder = bookSource.customOrder + book.type = bookSource.getBookType() + analyzeRule.setRuleData(book) + BookInfo.analyzeBookInfo( + book, + body, + analyzeRule, + bookSource, + baseUrl, + baseUrl, + false + ) + if (filter?.invoke(book.name, book.author) == false) { + return null + } + if (book.name.isNotBlank()) { + return book.toSearchBook() + } + return null + } + + @Throws(Exception::class) + private suspend fun getSearchItem( + bookSource: BookSource, + analyzeRule: AnalyzeRule, + item: Any, + baseUrl: String, + variable: String?, + log: Boolean, + filter: ((name: String, author: String) -> Boolean)?, + ruleName: List, + ruleBookUrl: List, + ruleAuthor: List, + ruleKind: List, + ruleCoverUrl: List, + ruleWordCount: List, + ruleIntro: List, + ruleLastChapter: List + ): SearchBook? { + val searchBook = SearchBook(variable = variable) + searchBook.type = bookSource.getBookType() + searchBook.origin = bookSource.bookSourceUrl + searchBook.originName = bookSource.bookSourceName + searchBook.originOrder = bookSource.customOrder + analyzeRule.setRuleData(searchBook) + analyzeRule.setContent(item) + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取书名", log) + searchBook.name = BookHelp.formatBookName(analyzeRule.getString(ruleName)) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.name}", log) + if (searchBook.name.isNotEmpty()) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取作者", log) + searchBook.author = BookHelp.formatBookAuthor(analyzeRule.getString(ruleAuthor)) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.author}", log) + if (filter?.invoke(searchBook.name, searchBook.author) == false) { + return null + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) + try { + searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") + Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind ?: ""}", log) + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) + try { + searchBook.wordCount = wordCountFormat(analyzeRule.getString(ruleWordCount)) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.wordCount}", log) + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取最新章节", log) + try { + searchBook.latestChapterTitle = analyzeRule.getString(ruleLastChapter) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.latestChapterTitle}", log) + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取简介", log) + try { + searchBook.intro = HtmlFormatter.format(analyzeRule.getString(ruleIntro)) + Debug.log(bookSource.bookSourceUrl, "└${searchBook.intro}", log) + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log) + try { + analyzeRule.getString(ruleCoverUrl).let { + if (it.isNotEmpty()) { + searchBook.coverUrl = NetworkUtils.getAbsoluteURL(baseUrl, it) + } + } + Debug.log(bookSource.bookSourceUrl, "└${searchBook.coverUrl ?: ""}", log) + } catch (e: Exception) { + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}", log) + } + coroutineContext.ensureActive() + Debug.log(bookSource.bookSourceUrl, "┌获取详情页链接", log) + searchBook.bookUrl = analyzeRule.getString(ruleBookUrl, isUrl = true) + if (searchBook.bookUrl.isEmpty()) { + searchBook.bookUrl = baseUrl + } + Debug.log(bookSource.bookSourceUrl, "└${searchBook.bookUrl}", log) + return searchBook + } + return null + } + + private fun checkExploreJson(bookSource: BookSource) { + if (Debug.callback == null) { + return + } + val json = bookSource.exploreKindsJson() + if (json.isEmpty()) { + return + } + val kinds = GSONStrict.fromJsonArray(json).getOrNull() + if (kinds != null) { + return + } + GSON.fromJsonArray(json).getOrNull()?.let { + Debug.log("≡发现地址规则 JSON 格式不规范,请改为规范格式") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/SearchModel.kt b/app/src/main/java/io/legado/app/model/webBook/SearchModel.kt new file mode 100755 index 000000000..2bd5fc0bc --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/SearchModel.kt @@ -0,0 +1,211 @@ +package io.legado.app.model.webBook + +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.SearchBook +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.ui.book.search.SearchScope +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.mapParallelSafe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import splitties.init.appCtx +import java.util.concurrent.Executors +import kotlin.coroutines.coroutineContext +import kotlin.math.min + +class SearchModel(private val scope: CoroutineScope, private val callBack: CallBack) { + val threadCount = AppConfig.threadCount + private var searchPool: ExecutorCoroutineDispatcher? = null + private var mSearchId = 0L + private var searchPage = 1 + private var searchKey: String = "" + private var bookSourceParts = emptyList() + private var searchBooks = arrayListOf() + private var searchJob: Job? = null + private var workingState = MutableStateFlow(true) + + + private fun initSearchPool() { + searchPool?.close() + searchPool = Executors + .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() + } + + fun search(searchId: Long, key: String) { + if (searchId != mSearchId) { + if (key.isEmpty()) { + return + } + searchKey = key + if (mSearchId != 0L) { + close() + } + searchBooks.clear() + bookSourceParts = callBack.getSearchScope().getBookSourceParts() + if (bookSourceParts.isEmpty()) { + callBack.onSearchCancel(NoStackTraceException("启用书源为空")) + return + } + mSearchId = searchId + searchPage = 1 + initSearchPool() + } else { + searchPage++ + } + startSearch() + } + + private fun startSearch() { + val precision = appCtx.getPrefBoolean(PreferKey.precisionSearch) + var hasMore = false + searchJob = scope.launch(searchPool!!) { + flow { + for (bs in bookSourceParts) { + bs.getBookSource()?.let { + emit(it) + } + workingState.first { it } + } + }.onStart { + callBack.onSearchStart() + }.mapParallelSafe(threadCount) { + withTimeout(30000L) { + WebBook.searchBookAwait( + it, searchKey, searchPage, + filter = { name, author -> + !precision || name.contains(searchKey) || + author.contains(searchKey) + }) + } + }.onEach { items -> + for (book in items) { + book.releaseHtmlData() + } + hasMore = hasMore || items.isNotEmpty() + appDb.searchBookDao.insert(*items.toTypedArray()) + mergeItems(items, precision) + currentCoroutineContext().ensureActive() + callBack.onSearchSuccess(searchBooks) + }.onCompletion { + if (it == null) callBack.onSearchFinish(searchBooks.isEmpty(), hasMore) + }.catch { + AppLog.put("书源搜索出错\n${it.localizedMessage}", it) + }.collect() + } + } + + private suspend fun mergeItems(newDataS: List, precision: Boolean) { + if (newDataS.isNotEmpty()) { + val copyData = ArrayList(searchBooks) + val equalData = arrayListOf() + val containsData = arrayListOf() + val otherData = arrayListOf() + copyData.forEach { + coroutineContext.ensureActive() + if (it.name == searchKey || it.author == searchKey) { + equalData.add(it) + } else if (it.name.contains(searchKey) || it.author.contains(searchKey)) { + containsData.add(it) + } else { + otherData.add(it) + } + } + newDataS.forEach { nBook -> + coroutineContext.ensureActive() + if (nBook.name == searchKey || nBook.author == searchKey) { + var hasSame = false + equalData.forEach { pBook -> + coroutineContext.ensureActive() + if (pBook.name == nBook.name && pBook.author == nBook.author) { + pBook.addOrigin(nBook.origin) + hasSame = true + } + } + if (!hasSame) { + equalData.add(nBook) + } + } else if (nBook.name.contains(searchKey) || nBook.author.contains(searchKey)) { + var hasSame = false + containsData.forEach { pBook -> + coroutineContext.ensureActive() + if (pBook.name == nBook.name && pBook.author == nBook.author) { + pBook.addOrigin(nBook.origin) + hasSame = true + } + } + if (!hasSame) { + containsData.add(nBook) + } + } else if (!precision) { + var hasSame = false + otherData.forEach { pBook -> + coroutineContext.ensureActive() + if (pBook.name == nBook.name && pBook.author == nBook.author) { + pBook.addOrigin(nBook.origin) + hasSame = true + } + } + if (!hasSame) { + otherData.add(nBook) + } + } + } + coroutineContext.ensureActive() + equalData.sortByDescending { it.origins.size } + equalData.addAll(containsData.sortedByDescending { it.origins.size }) + if (!precision) { + equalData.addAll(otherData) + } + coroutineContext.ensureActive() + searchBooks = equalData + } + } + + fun pause() { + workingState.value = false + } + + fun resume() { + workingState.value = true + } + + fun cancelSearch() { + close() + callBack.onSearchCancel() + } + + fun close() { + searchJob?.cancel() + searchPool?.close() + searchPool = null + mSearchId = 0L + } + + interface CallBack { + fun getSearchScope(): SearchScope + fun onSearchStart() + fun onSearchSuccess(searchBooks: List) + fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) + fun onSearchCancel(exception: Throwable? = null) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt new file mode 100755 index 000000000..1d9d39319 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt @@ -0,0 +1,415 @@ +package io.legado.app.model.webBook + +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.SearchBook +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.addType +import io.legado.app.help.book.removeAllBookType +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.http.StrResponse +import io.legado.app.help.source.getBookType +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.RuleData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.sync.Semaphore +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext + +@Suppress("MemberVisibilityCanBePrivate") +object WebBook { + + /** + * 搜索 + */ + fun searchBook( + scope: CoroutineScope, + bookSource: BookSource, + key: String, + page: Int? = 1, + context: CoroutineContext = Dispatchers.IO, + start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, + ): Coroutine> { + return Coroutine.async(scope, context, start = start, executeContext = executeContext) { + searchBookAwait(bookSource, key, page) + } + } + + suspend fun searchBookAwait( + bookSource: BookSource, + key: String, + page: Int? = 1, + filter: ((name: String, author: String) -> Boolean)? = null, + shouldBreak: ((size: Int) -> Boolean)? = null + ): ArrayList { + val searchUrl = bookSource.searchUrl + if (searchUrl.isNullOrBlank()) { + throw NoStackTraceException("搜索url不能为空") + } + val ruleData = RuleData() + val analyzeUrl = AnalyzeUrl( + mUrl = searchUrl, + key = key, + page = page, + baseUrl = bookSource.bookSourceUrl, + source = bookSource, + ruleData = ruleData, + coroutineContext = coroutineContext + ) + var res = analyzeUrl.getStrResponseAwait() + //检测书源是否已登录 + bookSource.loginCheckJs?.let { checkJs -> + if (checkJs.isNotBlank()) { + res = analyzeUrl.evalJS(checkJs, res) as StrResponse + } + } + checkRedirect(bookSource, res) + return BookList.analyzeBookList( + bookSource = bookSource, + ruleData = ruleData, + analyzeUrl = analyzeUrl, + baseUrl = res.url, + body = res.body, + isSearch = true, + isRedirect = res.raw.priorResponse?.isRedirect == true, + filter = filter, + shouldBreak = shouldBreak + ) + } + + /** + * 发现 + */ + fun exploreBook( + scope: CoroutineScope, + bookSource: BookSource, + url: String, + page: Int? = 1, + context: CoroutineContext = Dispatchers.IO, + ): Coroutine> { + return Coroutine.async(scope, context) { + exploreBookAwait(bookSource, url, page) + } + } + + suspend fun exploreBookAwait( + bookSource: BookSource, + url: String, + page: Int? = 1, + ): ArrayList { + val ruleData = RuleData() + val analyzeUrl = AnalyzeUrl( + mUrl = url, + page = page, + baseUrl = bookSource.bookSourceUrl, + source = bookSource, + ruleData = ruleData, + coroutineContext = coroutineContext + ) + var res = analyzeUrl.getStrResponseAwait() + //检测书源是否已登录 + bookSource.loginCheckJs?.let { checkJs -> + if (checkJs.isNotBlank()) { + res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse + } + } + checkRedirect(bookSource, res) + return BookList.analyzeBookList( + bookSource = bookSource, + ruleData = ruleData, + analyzeUrl = analyzeUrl, + baseUrl = res.url, + body = res.body, + isSearch = false + ) + } + + /** + * 书籍信息 + */ + fun getBookInfo( + scope: CoroutineScope, + bookSource: BookSource, + book: Book, + context: CoroutineContext = Dispatchers.IO, + canReName: Boolean = true, + ): Coroutine { + return Coroutine.async(scope, context) { + getBookInfoAwait(bookSource, book, canReName) + } + } + + suspend fun getBookInfoAwait( + bookSource: BookSource, + book: Book, + canReName: Boolean = true, + ): Book { + book.removeAllBookType() + book.addType(bookSource.getBookType()) + if (!book.infoHtml.isNullOrEmpty()) { + BookInfo.analyzeBookInfo( + bookSource = bookSource, + book = book, + baseUrl = book.bookUrl, + redirectUrl = book.bookUrl, + body = book.infoHtml, + canReName = canReName + ) + } else { + val analyzeUrl = AnalyzeUrl( + mUrl = book.bookUrl, + baseUrl = bookSource.bookSourceUrl, + source = bookSource, + ruleData = book, + coroutineContext = coroutineContext + ) + var res = analyzeUrl.getStrResponseAwait() + //检测书源是否已登录 + bookSource.loginCheckJs?.let { checkJs -> + if (checkJs.isNotBlank()) { + res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse + } + } + checkRedirect(bookSource, res) + BookInfo.analyzeBookInfo( + bookSource = bookSource, + book = book, + baseUrl = book.bookUrl, + redirectUrl = res.url, + body = res.body, + canReName = canReName + ) + } + return book + } + + /** + * 目录 + */ + fun getChapterList( + scope: CoroutineScope, + bookSource: BookSource, + book: Book, + runPerJs: Boolean = false, + context: CoroutineContext = Dispatchers.IO + ): Coroutine> { + return Coroutine.async(scope, context) { + getChapterListAwait(bookSource, book, runPerJs).getOrThrow() + } + } + + suspend fun runPreUpdateJs(bookSource: BookSource, book: Book): Result { + return kotlin.runCatching { + val preUpdateJs = bookSource.ruleToc?.preUpdateJs + if (!preUpdateJs.isNullOrBlank()) { + AnalyzeRule(book, bookSource, true) + .setCoroutineContext(coroutineContext) + .evalJS(preUpdateJs) + } + }.onFailure { + coroutineContext.ensureActive() + AppLog.put("执行preUpdateJs规则失败 书源:${bookSource.bookSourceName}", it) + } + } + + suspend fun getChapterListAwait( + bookSource: BookSource, + book: Book, + runPerJs: Boolean = false + ): Result> { + book.removeAllBookType() + book.addType(bookSource.getBookType()) + return kotlin.runCatching { + if (runPerJs) { + runPreUpdateJs(bookSource, book).getOrThrow() + } + if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { + BookChapterList.analyzeChapterList( + bookSource = bookSource, + book = book, + baseUrl = book.tocUrl, + redirectUrl = book.tocUrl, + body = book.tocHtml + ) + } else { + val analyzeUrl = AnalyzeUrl( + mUrl = book.tocUrl, + baseUrl = book.bookUrl, + source = bookSource, + ruleData = book, + coroutineContext = coroutineContext + ) + var res = analyzeUrl.getStrResponseAwait() + //检测书源是否已登录 + bookSource.loginCheckJs?.let { checkJs -> + if (checkJs.isNotBlank()) { + res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse + } + } + checkRedirect(bookSource, res) + BookChapterList.analyzeChapterList( + bookSource = bookSource, + book = book, + baseUrl = book.tocUrl, + redirectUrl = res.url, + body = res.body + ) + } + }.onFailure { + coroutineContext.ensureActive() + } + } + + /** + * 章节内容 + */ + fun getContent( + scope: CoroutineScope, + bookSource: BookSource, + book: Book, + bookChapter: BookChapter, + nextChapterUrl: String? = null, + needSave: Boolean = true, + context: CoroutineContext = Dispatchers.IO, + start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, + semaphore: Semaphore? = null, + ): Coroutine { + return Coroutine.async( + scope, + context, + start = start, + executeContext = executeContext, + semaphore = semaphore + ) { + getContentAwait(bookSource, book, bookChapter, nextChapterUrl, needSave) + } + } + + suspend fun getContentAwait( + bookSource: BookSource, + book: Book, + bookChapter: BookChapter, + nextChapterUrl: String? = null, + needSave: Boolean = true + ): String { + if (bookSource.getContentRule().content.isNullOrEmpty()) { + Debug.log(bookSource.bookSourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}") + return bookChapter.url + } + if (bookChapter.isVolume && bookChapter.url.startsWith(bookChapter.title)) { + Debug.log(bookSource.bookSourceUrl, "⇒一级目录正文不解析规则") + return bookChapter.tag ?: "" + } + return if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) { + BookContent.analyzeContent( + bookSource = bookSource, + book = book, + bookChapter = bookChapter, + baseUrl = bookChapter.getAbsoluteURL(), + redirectUrl = bookChapter.getAbsoluteURL(), + body = book.tocHtml, + nextChapterUrl = nextChapterUrl, + needSave = needSave + ) + } else { + val analyzeUrl = AnalyzeUrl( + mUrl = bookChapter.getAbsoluteURL(), + baseUrl = book.tocUrl, + source = bookSource, + ruleData = book, + chapter = bookChapter, + coroutineContext = coroutineContext + ) + var res = analyzeUrl.getStrResponseAwait( + jsStr = bookSource.getContentRule().webJs, + sourceRegex = bookSource.getContentRule().sourceRegex + ) + //检测书源是否已登录 + bookSource.loginCheckJs?.let { checkJs -> + if (checkJs.isNotBlank()) { + res = analyzeUrl.evalJS(checkJs, result = res) as StrResponse + } + } + checkRedirect(bookSource, res) + BookContent.analyzeContent( + bookSource = bookSource, + book = book, + bookChapter = bookChapter, + baseUrl = bookChapter.getAbsoluteURL(), + redirectUrl = res.url, + body = res.body, + nextChapterUrl = nextChapterUrl, + needSave = needSave + ) + } + } + + /** + * 精准搜索 + */ + fun preciseSearch( + scope: CoroutineScope, + bookSourceParts: List, + name: String, + author: String, + context: CoroutineContext = Dispatchers.IO, + semaphore: Semaphore? = null, + ): Coroutine> { + return Coroutine.async(scope, context, semaphore = semaphore) { + for (s in bookSourceParts) { + val source = s.getBookSource() ?: continue + val book = preciseSearchAwait(source, name, author).getOrNull() + if (book != null) { + return@async Pair(book, source) + } + } + throw NoStackTraceException("没有搜索到<$name>$author") + } + } + + suspend fun preciseSearchAwait( + bookSource: BookSource, + name: String, + author: String, + ): Result { + return kotlin.runCatching { + coroutineContext.ensureActive() + searchBookAwait( + bookSource, name, + filter = { fName, fAuthor -> fName == name && fAuthor == author }, + shouldBreak = { it > 0 } + ).firstOrNull()?.let { searchBook -> + coroutineContext.ensureActive() + return@runCatching searchBook.toBook() + } + throw NoStackTraceException("未搜索到 $name($author) 书籍") + }.onFailure { + coroutineContext.ensureActive() + } + } + + /** + * 检测重定向 + */ + private fun checkRedirect(bookSource: BookSource, response: StrResponse) { + response.raw.priorResponse?.let { + if (it.isRedirect) { + Debug.log(bookSource.bookSourceUrl, "≡检测到重定向(${it.code})") + Debug.log(bookSource.bookSourceUrl, "┌重定向后地址") + Debug.log(bookSource.bookSourceUrl, "└${response.url}") + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt new file mode 100755 index 000000000..d7548a19a --- /dev/null +++ b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt @@ -0,0 +1,121 @@ +package io.legado.app.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.view.KeyEvent +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.help.LifecycleHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.model.AudioPlay +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.service.AudioPlayService +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.utils.LogUtils +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.postEvent + + +/** + * Created by GKF on 2018/1/6. + * 监听耳机键 + */ +class MediaButtonReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (handleIntent(context, intent) && isOrderedBroadcast) { + abortBroadcast() + } + } + + companion object { + + private const val TAG = "MediaButtonReceiver" + + fun handleIntent(context: Context, intent: Intent): Boolean { + val intentAction = intent.action + if (Intent.ACTION_MEDIA_BUTTON == intentAction) { + @Suppress("DEPRECATION") + val keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) + ?: return false + val keycode: Int = keyEvent.keyCode + val action: Int = keyEvent.action + if (action == KeyEvent.ACTION_DOWN) { + LogUtils.d(TAG, "Receive mediaButton event, keycode:$keycode") + when (keycode) { + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + if (context.getPrefBoolean("mediaButtonPerNext", false)) { + ReadBook.moveToPrevChapter(true) + } else { + ReadAloud.prevParagraph(context) + } + } + + KeyEvent.KEYCODE_MEDIA_NEXT -> { + if (context.getPrefBoolean("mediaButtonPerNext", false)) { + ReadBook.moveToNextChapter(true) + } else { + ReadAloud.nextParagraph(context) + } + } + + else -> readAloud(context) + } + } + } + return true + } + + fun readAloud(context: Context, isMediaKey: Boolean = true) { + when { + BaseReadAloudService.isRun -> { + if (BaseReadAloudService.isPlay()) { + ReadAloud.pause(context) + AudioPlay.pause(context) + } else { + ReadAloud.resume(context) + AudioPlay.resume(context) + } + } + + AudioPlayService.isRun -> { + if (AudioPlayService.pause) { + AudioPlay.resume(context) + } else { + AudioPlay.pause(context) + } + } + + isMediaKey && !AppConfig.readAloudByMediaButton -> { + // break + } + + LifecycleHelp.isExistActivity(ReadBookActivity::class.java) -> + postEvent(EventBus.MEDIA_BUTTON, true) + + LifecycleHelp.isExistActivity(AudioPlayActivity::class.java) -> + postEvent(EventBus.MEDIA_BUTTON, true) + + else -> if (AppConfig.mediaButtonOnExit || LifecycleHelp.activitySize() > 0 || !isMediaKey) { + ReadAloud.upReadAloudClass() + if (ReadBook.book != null) { + ReadBook.readAloud() + } else { + appDb.bookDao.lastReadBook?.let { + ReadBook.resetData(it) + ReadBook.clearTextChapter() + ReadBook.loadContent(false) { + ReadBook.readAloud() + } + } + } + } + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/receiver/NetworkChangedListener.kt b/app/src/main/java/io/legado/app/receiver/NetworkChangedListener.kt new file mode 100755 index 000000000..a81ca2471 --- /dev/null +++ b/app/src/main/java/io/legado/app/receiver/NetworkChangedListener.kt @@ -0,0 +1,77 @@ +package io.legado.app.receiver + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import splitties.systemservices.connectivityManager + +/** + * 监测网络变化 + */ +@SuppressLint("ObsoleteSdkInt") +class NetworkChangedListener(private val context: Context) { + + var onNetworkChanged: (() -> Unit)? = null + + private val receiver: NetworkChangedReceiver? by lazy { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + NetworkChangedReceiver() + } + return@lazy null + } + + private val networkCallback: ConnectivityManager.NetworkCallback? by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return@lazy object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + onNetworkChanged?.invoke() + } + } + } + return@lazy null + } + + @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag") + fun register() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + networkCallback?.let { + connectivityManager.registerDefaultNetworkCallback(it) + } + } else { + receiver?.let { + context.registerReceiver(it, it.filter) + } + } + } + + fun unRegister() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + networkCallback?.let { + connectivityManager.unregisterNetworkCallback(it) + } + } else { + receiver?.let { + context.unregisterReceiver(it) + } + } + } + + inner class NetworkChangedReceiver : BroadcastReceiver() { + + val filter = IntentFilter().apply { + @Suppress("DEPRECATION") + addAction(ConnectivityManager.CONNECTIVITY_ACTION) + } + + override fun onReceive(context: Context, intent: Intent) { + onNetworkChanged?.invoke() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt b/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt new file mode 100755 index 000000000..b21bb7658 --- /dev/null +++ b/app/src/main/java/io/legado/app/receiver/SharedReceiverActivity.kt @@ -0,0 +1,60 @@ +package io.legado.app.receiver + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.main.MainActivity +import io.legado.app.utils.startActivity +import splitties.init.appCtx + +class SharedReceiverActivity : AppCompatActivity() { + + private val receivingType = "text/plain" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initIntent() + finish() + } + + @SuppressLint("ObsoleteSdkInt") + private fun initIntent() { + when { + intent.action == Intent.ACTION_SEND && intent.type == receivingType -> { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + dispose(it) + } + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && intent.action == Intent.ACTION_PROCESS_TEXT + && intent.type == receivingType -> { + intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)?.let { + dispose(it) + } + } + intent.getStringExtra("action") == "readAloud" -> { + MediaButtonReceiver.readAloud(appCtx, false) + } + } + } + + private fun dispose(text: String) { + if (text.isBlank()) { + return + } + val urls = text.split("\\s".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val result = StringBuilder() + for (url in urls) { + if (url.matches("http.+".toRegex())) + result.append("\n").append(url.trim { it <= ' ' }) + } + if (result.length > 1) { + startActivity() + } else { + SearchActivity.start(this, text) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt b/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt new file mode 100755 index 000000000..e6537d7b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/receiver/TimeBatteryReceiver.kt @@ -0,0 +1,31 @@ +package io.legado.app.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import io.legado.app.constant.EventBus +import io.legado.app.utils.postEvent + + +class TimeBatteryReceiver : BroadcastReceiver() { + + val filter = IntentFilter().apply { + addAction(Intent.ACTION_TIME_TICK) + addAction(Intent.ACTION_BATTERY_CHANGED) + } + + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_TIME_TICK -> { + postEvent(EventBus.TIME_CHANGED, "") + } + Intent.ACTION_BATTERY_CHANGED -> { + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + postEvent(EventBus.BATTERY_CHANGED, level) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/AudioPlayService.kt b/app/src/main/java/io/legado/app/service/AudioPlayService.kt new file mode 100755 index 000000000..544a17c08 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/AudioPlayService.kt @@ -0,0 +1,659 @@ +package io.legado.app.service + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.AudioManager +import android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import androidx.core.app.NotificationCompat +import androidx.lifecycle.lifecycleScope +import androidx.media.AudioFocusRequestCompat +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.constant.Status +import io.legado.app.help.MediaHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.exoplayer.ExoPlayerHelper +import io.legado.app.help.glide.ImageLoader +import io.legado.app.model.AudioPlay +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.getMediaItem +import io.legado.app.receiver.MediaButtonReceiver +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.utils.activityPendingIntent +import io.legado.app.utils.broadcastPendingIntent +import io.legado.app.utils.postEvent +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import splitties.init.appCtx +import splitties.systemservices.audioManager +import splitties.systemservices.notificationManager +import splitties.systemservices.powerManager +import splitties.systemservices.wifiManager + +/** + * 音频播放服务 + */ +class AudioPlayService : BaseService(), + AudioManager.OnAudioFocusChangeListener, + Player.Listener { + + companion object { + @JvmStatic + var isRun = false + private set + + @JvmStatic + var pause = true + private set + + @JvmStatic + var timeMinute: Int = 0 + + var url: String = "" + private set + + private const val MEDIA_SESSION_ACTIONS = (PlaybackStateCompat.ACTION_PLAY + or PlaybackStateCompat.ACTION_PAUSE + or PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_SEEK_TO) + + private const val APP_ACTION_STOP = "Stop" + private const val APP_ACTION_TIMER = "Timer" + } + + private val useWakeLock = AppConfig.audioPlayUseWakeLock + private val wakeLock by lazy { + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:AudioPlayService") + .apply { + this.setReferenceCounted(false) + } + } + private val wifiLock by lazy { + @Suppress("DEPRECATION") + wifiManager?.createWifiLock(WIFI_MODE_FULL_HIGH_PERF, "legado:AudioPlayService")?.apply { + setReferenceCounted(false) + } + } + private val mFocusRequest: AudioFocusRequestCompat by lazy { + MediaHelp.buildAudioFocusRequestCompat(this) + } + private val exoPlayer: ExoPlayer by lazy { + ExoPlayerHelper.createHttpExoPlayer(this) + } + private var mediaSessionCompat: MediaSessionCompat? = null + private var broadcastReceiver: BroadcastReceiver? = null + private var needResumeOnAudioFocusGain = false + private var position = AudioPlay.book?.durChapterPos ?: 0 + private var dsJob: Job? = null + private var upNotificationJob: Coroutine<*>? = null + private var upPlayProgressJob: Job? = null + private var playSpeed: Float = 1f + private var cover: Bitmap = + BitmapFactory.decodeResource(appCtx.resources, R.drawable.icon_read_book) + + override fun onCreate() { + super.onCreate() + isRun = true + exoPlayer.addListener(this) + AudioPlay.registerService(this) + initMediaSession() + initBroadcastReceiver() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + doDs() + execute { + ImageLoader + .loadBitmap(this@AudioPlayService, AudioPlay.book?.getDisplayCover()) + .submit() + .get() + }.onSuccess { + if (it.width > 16 && it.height > 16) { + cover = it + upMediaMetadata() + upAudioPlayNotification() + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.action?.let { action -> + when (action) { + IntentAction.play -> { + exoPlayer.stop() + upPlayProgressJob?.cancel() + pause = false + position = AudioPlay.book?.durChapterPos ?: 0 + url = AudioPlay.durPlayUrl + play() + } + + IntentAction.playNew -> { + exoPlayer.stop() + upPlayProgressJob?.cancel() + pause = false + position = 0 + url = AudioPlay.durPlayUrl + play() + } + + IntentAction.stopPlay -> { + exoPlayer.stop() + upPlayProgressJob?.cancel() + AudioPlay.status = Status.STOP + postEvent(EventBus.AUDIO_STATE, Status.STOP) + } + + IntentAction.pause -> pause() + IntentAction.resume -> resume() + IntentAction.prev -> AudioPlay.prev() + IntentAction.next -> AudioPlay.next() + IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) + IntentAction.addTimer -> addTimer() + IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) + IntentAction.adjustProgress -> { + adjustProgress(intent.getIntExtra("position", position)) + } + + IntentAction.stop -> stopSelf() + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + super.onDestroy() + if (useWakeLock) { + wakeLock.release() + wifiLock?.release() + } + isRun = false + abandonFocus() + exoPlayer.release() + mediaSessionCompat?.release() + unregisterReceiver(broadcastReceiver) + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) + AudioPlay.status = Status.STOP + postEvent(EventBus.AUDIO_STATE, Status.STOP) + AudioPlay.unregisterService() + upNotificationJob?.invokeOnCompletion { + notificationManager.cancel(NotificationId.AudioPlayService) + } + } + + /** + * 播放音频 + */ + @SuppressLint("WakelockTimeout") + private fun play() { + if (useWakeLock) { + wakeLock.acquire() + wifiLock?.acquire() + } + upAudioPlayNotification() + if (!requestFocus()) { + return + } + execute(context = Main) { + AudioPlay.status = Status.STOP + postEvent(EventBus.AUDIO_STATE, Status.STOP) + upPlayProgressJob?.cancel() + val analyzeUrl = AnalyzeUrl( + url, + source = AudioPlay.bookSource, + ruleData = AudioPlay.book, + chapter = AudioPlay.durChapter, + coroutineContext = coroutineContext + ) + exoPlayer.setMediaItem(analyzeUrl.getMediaItem()) + exoPlayer.playWhenReady = true + exoPlayer.seekTo(position.toLong()) + exoPlayer.prepare() + }.onError { + AppLog.put("播放出错\n${it.localizedMessage}", it) + toastOnUi("$url ${it.localizedMessage}") + stopSelf() + } + } + + /** + * 暂停播放 + */ + private fun pause(abandonFocus: Boolean = true) { + if (useWakeLock) { + wakeLock.release() + wifiLock?.release() + } + try { + pause = true + if (abandonFocus) { + abandonFocus() + } + upPlayProgressJob?.cancel() + position = exoPlayer.currentPosition.toInt() + if (exoPlayer.isPlaying) exoPlayer.pause() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) + AudioPlay.status = Status.PAUSE + postEvent(EventBus.AUDIO_STATE, Status.PAUSE) + upAudioPlayNotification() + } catch (e: Exception) { + e.printOnDebug() + } + } + + /** + * 恢复播放 + */ + @SuppressLint("WakelockTimeout") + private fun resume() { + if (useWakeLock) { + wakeLock.acquire() + wifiLock?.acquire() + } + try { + pause = false + if (url.isEmpty()) { + AudioPlay.loadOrUpPlayUrl() + return + } + if (!exoPlayer.isPlaying) { + exoPlayer.play() + } + upPlayProgress() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + AudioPlay.status = Status.PLAY + postEvent(EventBus.AUDIO_STATE, Status.PLAY) + upAudioPlayNotification() + } catch (e: Exception) { + e.printOnDebug() + stopSelf() + } + } + + /** + * 调节进度 + */ + private fun adjustProgress(position: Int) { + this.position = position + exoPlayer.seekTo(position.toLong()) + } + + /** + * 调节速度 + */ + @SuppressLint(value = ["ObsoleteSdkInt"]) + private fun upSpeed(adjust: Float) { + kotlin.runCatching { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + playSpeed += adjust + exoPlayer.setPlaybackSpeed(playSpeed) + postEvent(EventBus.AUDIO_SPEED, playSpeed) + } + } + } + + /** + * 播放状态监控 + */ + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + when (playbackState) { + Player.STATE_IDLE -> { + // 空闲 + } + + Player.STATE_BUFFERING -> { + // 缓冲中 + } + + Player.STATE_READY -> { + // 准备好 + AudioPlay.upLoading(false) + if (exoPlayer.playWhenReady) { + AudioPlay.status = Status.PLAY + postEvent(EventBus.AUDIO_STATE, Status.PLAY) + } else { + AudioPlay.status = Status.PAUSE + postEvent(EventBus.AUDIO_STATE, Status.PAUSE) + } + postEvent(EventBus.AUDIO_SIZE, exoPlayer.duration.toInt()) + upMediaMetadata() + upPlayProgress() + AudioPlay.saveDurChapter(exoPlayer.duration) + } + + Player.STATE_ENDED -> { + // 结束 + upPlayProgressJob?.cancel() + AudioPlay.playPositionChanged(exoPlayer.duration.toInt()) + AudioPlay.next() + } + } + upAudioPlayNotification() + } + + private fun upMediaMetadata() { + val metadata = MediaMetadataCompat.Builder() + .putBitmap(MediaMetadataCompat.METADATA_KEY_ART, cover) + .putText(MediaMetadataCompat.METADATA_KEY_TITLE, AudioPlay.durChapter?.title ?: "null") + .putText(MediaMetadataCompat.METADATA_KEY_ARTIST, AudioPlay.book?.name ?: "null") + .putText(MediaMetadataCompat.METADATA_KEY_ALBUM, AudioPlay.book?.author ?: "null") + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, exoPlayer.duration) + .build() + mediaSessionCompat?.setMetadata(metadata) + } + + /** + * 播放错误事件 + */ + override fun onPlayerError(error: PlaybackException) { + super.onPlayerError(error) + AudioPlay.status = Status.STOP + postEvent(EventBus.AUDIO_STATE, Status.STOP) + AudioPlay.upLoading(false) + val errorMsg = "音频播放出错\n${error.errorCodeName} ${error.errorCode}" + AppLog.put(errorMsg, error) + toastOnUi(errorMsg) + } + + private fun setTimer(minute: Int) { + timeMinute = minute + doDs() + } + + private fun addTimer() { + if (timeMinute == 180) { + timeMinute = 0 + } else { + timeMinute += 10 + if (timeMinute > 180) timeMinute = 180 + } + doDs() + } + + /** + * 定时 + */ + private fun doDs() { + postEvent(EventBus.AUDIO_DS, timeMinute) + upAudioPlayNotification() + dsJob?.cancel() + dsJob = lifecycleScope.launch { + while (isActive) { + delay(60000) + if (!pause) { + if (timeMinute >= 0) { + timeMinute-- + } + if (timeMinute == 0) { + AudioPlay.stop() + postEvent(EventBus.AUDIO_DS, timeMinute) + break + } + } + postEvent(EventBus.AUDIO_DS, timeMinute) + upAudioPlayNotification() + } + } + } + + /** + * 每隔1秒发送播放进度 + */ + private fun upPlayProgress() { + upPlayProgressJob?.cancel() + upPlayProgressJob = lifecycleScope.launch { + while (isActive) { + //更新buffer位置 + AudioPlay.playPositionChanged(exoPlayer.currentPosition.toInt()) + postEvent(EventBus.AUDIO_BUFFER_PROGRESS, exoPlayer.bufferedPosition.toInt()) + postEvent(EventBus.AUDIO_PROGRESS, AudioPlay.durChapterPos) + postEvent(EventBus.AUDIO_SIZE, exoPlayer.duration.toInt()) + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + delay(1000) + } + } + } + + /** + * 更新媒体状态 + */ + private fun upMediaSessionPlaybackState(state: Int) { + mediaSessionCompat?.setPlaybackState( + PlaybackStateCompat.Builder() + .setActions(MEDIA_SESSION_ACTIONS) + .setState(state, exoPlayer.currentPosition, 1f) + .setBufferedPosition(exoPlayer.bufferedPosition) + .addCustomAction( + APP_ACTION_STOP, + getString(R.string.stop), + R.drawable.ic_stop_black_24dp + ) + .addCustomAction( + APP_ACTION_TIMER, + getString(R.string.set_timer), + R.drawable.ic_time_add_24dp + ) + .build() + ) + } + + /** + * 初始化MediaSession, 注册多媒体按钮 + */ + @SuppressLint("UnspecifiedImmutableFlag") + private fun initMediaSession() { + mediaSessionCompat = MediaSessionCompat(this, "readAloud") + mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() { + override fun onSeekTo(pos: Long) { + position = pos.toInt() + exoPlayer.seekTo(pos) + } + + override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { + return MediaButtonReceiver.handleIntent(this@AudioPlayService, mediaButtonEvent) + } + + override fun onPlay() = resume() + + override fun onPause() = pause() + + override fun onCustomAction(action: String?, extras: Bundle?) { + action ?: return + + when (action) { + APP_ACTION_STOP -> stopSelf() + APP_ACTION_TIMER -> addTimer() + } + } + }) + mediaSessionCompat?.setMediaButtonReceiver( + broadcastPendingIntent(Intent.ACTION_MEDIA_BUTTON) + ) + mediaSessionCompat?.isActive = true + } + + /** + * 断开耳机监听 + */ + private fun initBroadcastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) { + pause() + } + } + } + val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + registerReceiver(broadcastReceiver, intentFilter) + } + + /** + * 音频焦点变化 + */ + override fun onAudioFocusChange(focusChange: Int) { + if (AppConfig.ignoreAudioFocus) { + AppLog.put("忽略音频焦点处理(有声)") + return + } + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + if (needResumeOnAudioFocusGain) { + AppLog.put("音频焦点获得,继续播放") + resume() + } else { + AppLog.put("音频焦点获得") + } + } + + AudioManager.AUDIOFOCUS_LOSS -> { + AppLog.put("音频焦点丢失,暂停播放") + pause() + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停播放") + if (!pause) { + needResumeOnAudioFocusGain = true + pause(false) + } + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音), + AppLog.put("音频焦点短暂丢失,不做处理") + } + } + } + + private fun createNotification(): NotificationCompat.Builder { + var nTitle: String = when { + pause -> getString(R.string.audio_pause) + timeMinute in 1..60 -> getString( + R.string.playing_timer, + timeMinute + ) + + else -> getString(R.string.audio_play_t) + } + nTitle += ": ${AudioPlay.book?.name}" + var nSubtitle = AudioPlay.durChapter?.title + if (nSubtitle.isNullOrEmpty()) { + nSubtitle = getString(R.string.audio_play_s) + } + val builder = NotificationCompat + .Builder(this@AudioPlayService, AppConst.channelIdReadAloud) + .setSmallIcon(R.drawable.ic_volume_up) + .setSubText(getString(R.string.audio)) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentTitle(nTitle) + .setContentText(nSubtitle) + .setContentIntent( + activityPendingIntent("activity") + ) + builder.setLargeIcon(cover) + if (pause) { + builder.addAction( + R.drawable.ic_play_24dp, + getString(R.string.resume), + servicePendingIntent(IntentAction.resume) + ) + } else { + builder.addAction( + R.drawable.ic_pause_24dp, + getString(R.string.pause), + servicePendingIntent(IntentAction.pause) + ) + } + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.stop), + servicePendingIntent(IntentAction.stop) + ) + builder.addAction( + R.drawable.ic_time_add_24dp, + getString(R.string.set_timer), + servicePendingIntent(IntentAction.addTimer) + ) + builder.setStyle( + androidx.media.app.NotificationCompat.MediaStyle() + .setShowActionsInCompactView(0, 1, 2) + .setMediaSession(mediaSessionCompat?.sessionToken) + ) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + return builder + } + + private fun upAudioPlayNotification() { + upNotificationJob = execute { + try { + val notification = createNotification() + notificationManager.notify(NotificationId.AudioPlayService, notification.build()) + } catch (e: Exception) { + AppLog.put("创建音频播放通知出错,${e.localizedMessage}", e, true) + } + } + } + + /** + * 更新通知 + */ + override fun startForegroundNotification() { + execute { + try { + val notification = createNotification() + startForeground(NotificationId.AudioPlayService, notification.build()) + } catch (e: Exception) { + AppLog.put("创建音频播放通知出错,${e.localizedMessage}", e, true) + //创建通知出错不结束服务就会崩溃,服务必须绑定通知 + stopSelf() + } + } + } + + /** + * 请求音频焦点 + * @return 音频焦点 + */ + private fun requestFocus(): Boolean { + if (AppConfig.ignoreAudioFocus) { + return true + } + return MediaHelp.requestFocus(mFocusRequest) + } + + /** + * 放弃音频焦点 + */ + private fun abandonFocus() { + @Suppress("DEPRECATION") + audioManager.abandonAudioFocus(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt new file mode 100755 index 000000000..6f54a479f --- /dev/null +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -0,0 +1,774 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.service + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.AudioManager +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.PowerManager +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import android.telephony.PhoneStateListener +import android.telephony.TelephonyManager +import androidx.annotation.CallSuper +import androidx.core.app.NotificationCompat +import androidx.lifecycle.lifecycleScope +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.constant.PreferKey +import io.legado.app.constant.Status +import io.legado.app.help.MediaHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.glide.ImageLoader +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.receiver.MediaButtonReceiver +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.utils.LogUtils +import io.legado.app.utils.activityPendingIntent +import io.legado.app.utils.broadcastPendingIntent +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.observeEvent +import io.legado.app.utils.observeSharedPreferences +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import splitties.init.appCtx +import splitties.systemservices.audioManager +import splitties.systemservices.notificationManager +import splitties.systemservices.powerManager +import splitties.systemservices.telephonyManager +import splitties.systemservices.wifiManager + +/** + * 朗读服务 + */ +abstract class BaseReadAloudService : BaseService(), + AudioManager.OnAudioFocusChangeListener { + + companion object { + @JvmStatic + var isRun = false + private set + + @JvmStatic + var pause = true + private set + + @JvmStatic + var timeMinute: Int = 0 + private set + + fun isPlay(): Boolean { + return isRun && !pause + } + + private const val TAG = "BaseReadAloudService" + + } + + private val useWakeLock = appCtx.getPrefBoolean(PreferKey.readAloudWakeLock, false) + private val wakeLock by lazy { + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:ReadAloudService") + .apply { + this.setReferenceCounted(false) + } + } + private val wifiLock by lazy { + @Suppress("DEPRECATION") + wifiManager?.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "legado:AudioPlayService") + ?.apply { + setReferenceCounted(false) + } + } + private val mFocusRequest: AudioFocusRequestCompat by lazy { + MediaHelp.buildAudioFocusRequestCompat(this) + } + private val mediaSessionCompat: MediaSessionCompat by lazy { + MediaSessionCompat(this, "readAloud") + } + private val phoneStateListener by lazy { + ReadAloudPhoneStateListener() + } + internal var contentList = emptyList() + internal var nowSpeak: Int = 0 + internal var readAloudNumber: Int = 0 + internal var textChapter: TextChapter? = null + internal var pageIndex = 0 + private var needResumeOnAudioFocusGain = false + private var needResumeOnCallStateIdle = false + private var registeredPhoneStateListener = false + private var dsJob: Job? = null + private var upNotificationJob: Coroutine<*>? = null + private var cover: Bitmap = + BitmapFactory.decodeResource(appCtx.resources, R.drawable.icon_read_book) + var pageChanged = false + private var toLast = false + var paragraphStartPos = 0 + var readAloudByPage = false + private set + + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) { + pauseReadAloud() + } + } + } + + @SuppressLint("WakelockTimeout") + override fun onCreate() { + super.onCreate() + isRun = true + pause = false + observeLiveBus() + initMediaSession() + initBroadcastReceiver() + initPhoneStateListener() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + setTimer(AppConfig.ttsTimer) + if (AppConfig.ttsTimer > 0) { + toastOnUi("朗读定时 ${AppConfig.ttsTimer} 分钟") + } + execute { + ImageLoader + .loadBitmap(this@BaseReadAloudService, ReadBook.book?.getDisplayCover()) + .submit() + .get() + }.onSuccess { + if (it.width > 16 && it.height > 16) { + cover = it + upReadAloudNotification() + } + } + } + + fun observeLiveBus() { + observeEvent(EventBus.READ_ALOUD_PLAY) { + val play = it.getBoolean("play") + val pageIndex = it.getInt("pageIndex") + val startPos = it.getInt("startPos") + newReadAloud(play, pageIndex, startPos) + } + observeSharedPreferences { _, key -> + when (key) { + PreferKey.ignoreAudioFocus, + PreferKey.pauseReadAloudWhilePhoneCalls -> { + initPhoneStateListener() + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + if (useWakeLock) { + wakeLock.release() + wifiLock?.release() + } + isRun = false + pause = true + abandonFocus() + unregisterReceiver(broadcastReceiver) + postEvent(EventBus.ALOUD_STATE, Status.STOP) + notificationManager.cancel(NotificationId.ReadAloudService) + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) + mediaSessionCompat.release() + ReadBook.uploadProgress() + unregisterPhoneStateListener(phoneStateListener) + upNotificationJob?.invokeOnCompletion { + notificationManager.cancel(NotificationId.ReadAloudService) + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.play -> newReadAloud( + intent.getBooleanExtra("play", true), + intent.getIntExtra("pageIndex", ReadBook.durPageIndex), + intent.getIntExtra("startPos", 0) + ) + + IntentAction.pause -> pauseReadAloud() + IntentAction.resume -> resumeReadAloud() + IntentAction.upTtsSpeechRate -> upSpeechRate(true) + IntentAction.prevParagraph -> prevP() + IntentAction.nextParagraph -> nextP() + IntentAction.prev -> prevChapter() + IntentAction.next -> nextChapter() + IntentAction.addTimer -> addTimer() + IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) + IntentAction.stop -> stopSelf() + } + return super.onStartCommand(intent, flags, startId) + } + + private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) { + execute(executeContext = IO) { + this@BaseReadAloudService.pageIndex = pageIndex + textChapter = ReadBook.curTextChapter + val textChapter = textChapter ?: return@execute + if (!textChapter.isCompleted) { + return@execute + } + readAloudNumber = textChapter.getReadLength(pageIndex) + startPos + readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage) + contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0) + .split("\n") + .filter { it.isNotEmpty() } + var pos = startPos + val page = textChapter.getPage(pageIndex)!! + if (pos > 0) { + for (paragraph in page.paragraphs) { + val tmp = pos - paragraph.length - 1 + if (tmp < 0) break + pos = tmp + } + } + nowSpeak = textChapter.getParagraphNum(readAloudNumber + 1, readAloudByPage) - 1 + if (!readAloudByPage && startPos == 0 && !toLast) { + pos = page.chapterPosition - + textChapter.paragraphs[nowSpeak].chapterPosition + } + if (toLast) { + toLast = false + readAloudNumber = textChapter.getLastParagraphPosition() + nowSpeak = contentList.lastIndex + if (page.paragraphs.size == 1) { + pos = page.chapterPosition - + textChapter.paragraphs[nowSpeak].chapterPosition + } + } + paragraphStartPos = pos + launch(Main) { + if (play) play() else pageChanged = true + } + }.onError { + AppLog.put("启动朗读出错\n${it.localizedMessage}", it, true) + } + } + + @SuppressLint("WakelockTimeout") + open fun play() { + if (useWakeLock) { + wakeLock.acquire() + wifiLock?.acquire() + } + isRun = true + pause = false + needResumeOnAudioFocusGain = false + needResumeOnCallStateIdle = false + upReadAloudNotification() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + postEvent(EventBus.ALOUD_STATE, Status.PLAY) + } + + abstract fun playStop() + + @CallSuper + open fun pauseReadAloud(abandonFocus: Boolean = true) { + if (useWakeLock) { + wakeLock.release() + wifiLock?.release() + } + pause = true + if (abandonFocus) { + abandonFocus() + } + upReadAloudNotification() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) + postEvent(EventBus.ALOUD_STATE, Status.PAUSE) + ReadBook.uploadProgress() + doDs() + } + + @SuppressLint("WakelockTimeout") + @CallSuper + open fun resumeReadAloud() { + resumeReadAloudInternal() + } + + private fun resumeReadAloudInternal() { + pause = false + needResumeOnAudioFocusGain = false + needResumeOnCallStateIdle = false + upReadAloudNotification() + upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) + postEvent(EventBus.ALOUD_STATE, Status.PLAY) + } + + abstract fun upSpeechRate(reset: Boolean = false) + + fun upTtsProgress(progress: Int) { + postEvent(EventBus.TTS_PROGRESS, progress) + } + + private fun prevP() { + if (nowSpeak > 0) { + playStop() + do { + nowSpeak-- + readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos + paragraphStartPos = 0 + } while (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) + textChapter?.let { + if (readAloudByPage) { + val paragraphs = it.getParagraphs(true) + if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber++ + } + if (readAloudNumber < it.getReadLength(pageIndex)) { + pageIndex-- + ReadBook.moveToPrevPage() + } + } + upTtsProgress(readAloudNumber + 1) + play() + } else { + toLast = true + ReadBook.moveToPrevChapter(true) + } + } + + private fun nextP() { + if (nowSpeak < contentList.size - 1) { + playStop() + readAloudNumber += contentList[nowSpeak].length.plus(1) - paragraphStartPos + paragraphStartPos = 0 + nowSpeak++ + textChapter?.let { + if (readAloudByPage) { + val paragraphs = it.getParagraphs(true) + if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber-- + } + if (pageIndex + 1 < it.pageSize + && readAloudNumber >= it.getReadLength(pageIndex + 1) + ) { + pageIndex++ + ReadBook.moveToNextPage() + } + } + upTtsProgress(readAloudNumber + 1) + play() + } else { + nextChapter() + } + } + + private fun setTimer(minute: Int) { + timeMinute = minute + doDs() + } + + private fun addTimer() { + if (timeMinute == 180) { + timeMinute = 0 + } else { + timeMinute += 10 + if (timeMinute > 180) timeMinute = 180 + } + doDs() + } + + /** + * 定时 + */ + @Synchronized + private fun doDs() { + postEvent(EventBus.READ_ALOUD_DS, timeMinute) + upReadAloudNotification() + dsJob?.cancel() + dsJob = lifecycleScope.launch { + while (isActive) { + delay(60000) + if (!pause) { + if (timeMinute >= 0) { + timeMinute-- + } + if (timeMinute == 0) { + ReadAloud.stop(this@BaseReadAloudService) + postEvent(EventBus.READ_ALOUD_DS, timeMinute) + break + } + } + postEvent(EventBus.READ_ALOUD_DS, timeMinute) + upReadAloudNotification() + } + } + } + + /** + * 请求音频焦点 + * @return 音频焦点 + */ + fun requestFocus(): Boolean { + if (AppConfig.ignoreAudioFocus) { + return true + } + val requestFocus = MediaHelp.requestFocus(mFocusRequest) + if (!requestFocus) { + pauseReadAloud(false) + toastOnUi("未获取到音频焦点") + } + return requestFocus + } + + /** + * 放弃音频焦点 + */ + private fun abandonFocus() { + AudioManagerCompat.abandonAudioFocusRequest(audioManager, mFocusRequest) + } + + /** + * 更新媒体状态 + */ + private fun upMediaSessionPlaybackState(state: Int) { + mediaSessionCompat.setPlaybackState( + PlaybackStateCompat.Builder() + .setActions(MediaHelp.MEDIA_SESSION_ACTIONS) + .setState(state, nowSpeak.toLong(), 1f) + // 为系统媒体控件添加定时按钮 + .addCustomAction( + PlaybackStateCompat.CustomAction.Builder( + "ACTION_ADD_TIMER", + getString(R.string.set_timer), + R.drawable.ic_time_add_24dp + ).build() + ) + .build() + ) + } + + /** + * 初始化MediaSession, 注册多媒体按钮 + */ + @SuppressLint("UnspecifiedImmutableFlag") + private fun initMediaSession() { + if (getPrefBoolean("systemMediaControlCompatibilityChange")) { + mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { + override fun onPlay() { + resumeReadAloud() + } + + override fun onPause() { + pauseReadAloud() + } + + override fun onSkipToNext() { + if (getPrefBoolean("mediaButtonPerNext", false)) { + nextChapter() + } else { + nextP() + } + } + + override fun onSkipToPrevious() { + if (getPrefBoolean("mediaButtonPerNext", false)) { + prevChapter() + } else { + prevP() + } + } + + override fun onStop() { + stopSelf() + } + + override fun onCustomAction(action: String, extras: Bundle?) { + if (action == "ACTION_ADD_TIMER") addTimer() + } + + override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { + return MediaButtonReceiver.handleIntent( + this@BaseReadAloudService, mediaButtonEvent + ) + } + }) + } else { + mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { + override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { + return MediaButtonReceiver.handleIntent( + this@BaseReadAloudService, mediaButtonEvent + ) + } + }) + } + mediaSessionCompat.setMediaButtonReceiver( + broadcastPendingIntent(Intent.ACTION_MEDIA_BUTTON) + ) + mediaSessionCompat.isActive = true + } + + /** + * 注册多媒体按钮监听 + */ + private fun initBroadcastReceiver() { + val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + registerReceiver(broadcastReceiver, intentFilter) + } + + /** + * 音频焦点变化 + */ + override fun onAudioFocusChange(focusChange: Int) { + if (AppConfig.ignoreAudioFocus) { + AppLog.put("忽略音频焦点处理(TTS)") + return + } + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + if (needResumeOnAudioFocusGain) { + AppLog.put("音频焦点获得,继续朗读") + resumeReadAloud() + } else { + AppLog.put("音频焦点获得") + } + } + + AudioManager.AUDIOFOCUS_LOSS -> { + AppLog.put("音频焦点丢失,暂停朗读") + pauseReadAloud() + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停朗读") + if (!pause) { + needResumeOnAudioFocusGain = true + pauseReadAloud(false) + } + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音), + AppLog.put("音频焦点短暂丢失,不做处理") + } + } + } + + private fun upReadAloudNotification() { + upNotificationJob = execute { + try { + val notification = createNotification() + notificationManager.notify(NotificationId.ReadAloudService, notification.build()) + } catch (e: Exception) { + AppLog.put("创建朗读通知出错,${e.localizedMessage}", e, true) + } + } + } + + private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle { + val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() + .setShowActionsInCompactView(1, 2, 4) + if (getPrefBoolean("systemMediaControlCompatibilityChange")) { + //fix #4090 android 14 can not show play control in lock screen + mediaStyle.setMediaSession(mediaSessionCompat.sessionToken) + } + return mediaStyle + } + + private fun createNotification(): NotificationCompat.Builder { + var nTitle: String = when { + pause -> getString(R.string.read_aloud_pause) + timeMinute > 0 -> getString( + R.string.read_aloud_timer, + timeMinute + ) + + else -> getString(R.string.read_aloud_t) + } + nTitle += ": ${ReadBook.book?.name}" + var nSubtitle = ReadBook.curTextChapter?.title + if (nSubtitle.isNullOrBlank()) + nSubtitle = getString(R.string.read_aloud_s) + val builder = NotificationCompat + .Builder(this, AppConst.channelIdReadAloud) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .setCategory(NotificationCompat.CATEGORY_TRANSPORT) + .setSmallIcon(R.drawable.ic_volume_up) + .setSubText(getString(R.string.read_aloud)) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentTitle(nTitle) + .setContentText(nSubtitle) + .setContentIntent( + activityPendingIntent("activity") + ) + .setVibrate(null) + .setSound(null) + .setLights(0, 0, 0) + builder.setLargeIcon(cover) + // 按钮定义:上一章、播放、停止、下一章、定时 + builder.addAction( + R.drawable.ic_skip_previous, + getString(R.string.previous_chapter), + aloudServicePendingIntent(IntentAction.prev) + ) + if (pause) { + builder.addAction( + R.drawable.ic_play_24dp, + getString(R.string.resume), + aloudServicePendingIntent(IntentAction.resume) + ) + } else { + builder.addAction( + R.drawable.ic_pause_24dp, + getString(R.string.pause), + aloudServicePendingIntent(IntentAction.pause) + ) + } + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.stop), + aloudServicePendingIntent(IntentAction.stop) + ) + builder.addAction( + R.drawable.ic_skip_next, + getString(R.string.next_chapter), + aloudServicePendingIntent(IntentAction.next) + ) + builder.addAction( + R.drawable.ic_time_add_24dp, + getString(R.string.set_timer), + aloudServicePendingIntent(IntentAction.addTimer) + ) + builder.setStyle(choiceMediaStyle()) + return builder + } + + /** + * 更新通知 + */ + override fun startForegroundNotification() { + execute { + try { + val notification = createNotification() + startForeground(NotificationId.ReadAloudService, notification.build()) + } catch (e: Exception) { + AppLog.put("创建朗读通知出错,${e.localizedMessage}", e, true) + //创建通知出错不结束服务就会崩溃,服务必须绑定通知 + stopSelf() + } + } + } + + abstract fun aloudServicePendingIntent(actionStr: String): PendingIntent? + + open fun prevChapter() { + toLast = false + resumeReadAloudInternal() + ReadBook.moveToPrevChapter(true, toLast = false) + } + + open fun nextChapter() { + ReadBook.upReadTime() + AppLog.putDebug("${ReadBook.curTextChapter?.chapter?.title} 朗读结束跳转下一章并朗读") + resumeReadAloudInternal() + if (!ReadBook.moveToNextChapter(true)) { + stopSelf() + } + } + + private fun initPhoneStateListener() { + val needRegister = AppConfig.ignoreAudioFocus && AppConfig.pauseReadAloudWhilePhoneCalls + if (needRegister && registeredPhoneStateListener) { + return + } + if (needRegister) { + registerPhoneStateListener(phoneStateListener) + } else { + unregisterPhoneStateListener(phoneStateListener) + } + } + + private fun unregisterPhoneStateListener(l: PhoneStateListener) { + if (registeredPhoneStateListener) { + withReadPhoneStatePermission { + telephonyManager.listen(l, PhoneStateListener.LISTEN_NONE) + registeredPhoneStateListener = false + } + } + } + + private fun registerPhoneStateListener(l: PhoneStateListener) { + withReadPhoneStatePermission { + telephonyManager.listen(l, PhoneStateListener.LISTEN_CALL_STATE) + registeredPhoneStateListener = true + } + } + + private fun withReadPhoneStatePermission(block: () -> Unit) { + try { + block.invoke() + } catch (_: SecurityException) { + PermissionsCompat.Builder() + .addPermissions(Permissions.READ_PHONE_STATE) + .rationale(R.string.read_aloud_read_phone_state_permission_rationale) + .onGranted { + try { + block.invoke() + } catch (_: SecurityException) { + LogUtils.d(TAG, "Grant read phone state permission fail.") + } + } + .request() + } + } + + @Suppress("OVERRIDE_DEPRECATION") + inner class ReadAloudPhoneStateListener : PhoneStateListener() { + override fun onCallStateChanged(state: Int, phoneNumber: String?) { + super.onCallStateChanged(state, phoneNumber) + when (state) { + TelephonyManager.CALL_STATE_IDLE -> { + if (needResumeOnCallStateIdle) { + AppLog.put("来电结束,继续朗读") + resumeReadAloud() + } else { + AppLog.put("来电结束") + } + } + + TelephonyManager.CALL_STATE_RINGING -> { + if (!pause) { + AppLog.put("来电响铃,暂停朗读") + needResumeOnCallStateIdle = true + pauseReadAloud() + } else { + AppLog.put("来电响铃") + } + } + + TelephonyManager.CALL_STATE_OFFHOOK -> { + AppLog.put("来电接听,不做处理") + } + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/service/CacheBookService.kt b/app/src/main/java/io/legado/app/service/CacheBookService.kt new file mode 100755 index 000000000..1474ab0a4 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/CacheBookService.kt @@ -0,0 +1,187 @@ +package io.legado.app.service + +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.data.appDb +import io.legado.app.help.book.update +import io.legado.app.help.config.AppConfig +import io.legado.app.model.CacheBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.ui.book.cache.CacheActivity +import io.legado.app.utils.activityPendingIntent +import io.legado.app.utils.postEvent +import io.legado.app.utils.servicePendingIntent +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import splitties.init.appCtx +import splitties.systemservices.notificationManager +import java.util.concurrent.Executors +import kotlin.math.min + +/** + * 缓存书籍服务 + */ +class CacheBookService : BaseService() { + + companion object { + var isRun = false + private set + } + + private val threadCount = AppConfig.threadCount + private var cachePool = + Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() + private var downloadJob: Job? = null + private var notificationContent = appCtx.getString(R.string.service_starting) + private var mutex = Mutex() + private val notificationBuilder by lazy { + val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_download) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentTitle(getString(R.string.offline_cache)) + .setContentIntent(activityPendingIntent("cacheActivity")) + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + servicePendingIntent(IntentAction.stop) + ) + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } + + override fun onCreate() { + super.onCreate() + isRun = true + lifecycleScope.launch { + while (isActive) { + delay(1000) + notificationContent = CacheBook.downloadSummary + upCacheBookNotification() + postEvent(EventBus.UP_DOWNLOAD, "") + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.action?.let { action -> + when (action) { + IntentAction.start -> addDownloadData( + intent.getStringExtra("bookUrl"), + intent.getIntExtra("start", 0), + intent.getIntExtra("end", 0) + ) + + IntentAction.remove -> removeDownload(intent.getStringExtra("bookUrl")) + IntentAction.stop -> stopSelf() + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + isRun = false + cachePool.close() + CacheBook.close() + super.onDestroy() + postEvent(EventBus.UP_DOWNLOAD, "") + } + + private fun addDownloadData(bookUrl: String?, start: Int, end: Int) { + bookUrl ?: return + execute { + val cacheBook = CacheBook.getOrCreate(bookUrl) ?: return@execute + val chapterCount = appDb.bookChapterDao.getChapterCount(bookUrl) + val book = cacheBook.book + if (chapterCount == 0) { + cacheBook.setLoading() + mutex.withLock { + val name = book.name + if (book.tocUrl.isEmpty()) { + kotlin.runCatching { + WebBook.getBookInfoAwait(cacheBook.bookSource, book) + }.onFailure { + removeDownload(bookUrl) + val msg = "《$name》目录为空且加载详情页失败\n${it.localizedMessage}" + AppLog.put(msg, it, true) + return@execute + } + } + WebBook.getChapterListAwait(cacheBook.bookSource, book).onFailure { + if (book.totalChapterNum > 0) { + book.totalChapterNum = 0 + book.update() + } + removeDownload(bookUrl) + val msg = "《$name》目录为空且加载目录失败\n${it.localizedMessage}" + AppLog.put(msg, it, true) + return@execute + }.getOrNull()?.let { toc -> + appDb.bookChapterDao.insert(*toc.toTypedArray()) + } + book.update() + } + } + val end2 = if (end < 0) { + book.lastChapterIndex + } else { + min(end, book.lastChapterIndex) + } + cacheBook.addDownload(start, end2) + notificationContent = CacheBook.downloadSummary + upCacheBookNotification() + }.onFinally { + if (downloadJob == null) { + download() + } + } + } + + private fun removeDownload(bookUrl: String?) { + CacheBook.cacheBookMap[bookUrl]?.stop() + postEvent(EventBus.UP_DOWNLOAD, "") + if (downloadJob == null && CacheBook.isRun) { + download() + return + } + if (CacheBook.cacheBookMap.isEmpty()) { + stopSelf() + } + } + + private fun download() { + downloadJob?.cancel() + downloadJob = lifecycleScope.launch(cachePool) { + CacheBook.startProcessJob(cachePool) + stopSelf() + } + } + + private fun upCacheBookNotification() { + notificationBuilder.setContentText(notificationContent) + val notification = notificationBuilder.build() + notificationManager.notify(NotificationId.CacheBookService, notification) + } + + /** + * 更新通知 + */ + override fun startForegroundNotification() { + notificationBuilder.setContentText(notificationContent) + val notification = notificationBuilder.build() + startForeground(NotificationId.CacheBookService, notification) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/CheckSourceService.kt b/app/src/main/java/io/legado/app/service/CheckSourceService.kt new file mode 100755 index 000000000..4d08b1d6d --- /dev/null +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -0,0 +1,261 @@ +package io.legado.app.service + +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.lifecycle.lifecycleScope +import com.script.ScriptException +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.BookSourceType +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.ContentEmptyException +import io.legado.app.exception.NoStackTraceException +import io.legado.app.exception.TocEmptyException +import io.legado.app.help.IntentData +import io.legado.app.help.config.AppConfig +import io.legado.app.help.source.exploreKinds +import io.legado.app.model.CheckSource +import io.legado.app.model.Debug +import io.legado.app.model.webBook.WebBook +import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.utils.activityPendingIntent +import io.legado.app.utils.onEachParallel +import io.legado.app.utils.postEvent +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import org.mozilla.javascript.WrappedException +import splitties.init.appCtx +import splitties.systemservices.notificationManager +import java.util.concurrent.Executors +import kotlin.coroutines.coroutineContext +import kotlin.math.min + +/** + * 校验书源 + */ +class CheckSourceService : BaseService() { + private var threadCount = AppConfig.threadCount + private var searchCoroutine = + Executors.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() + private var notificationMsg = appCtx.getString(R.string.service_starting) + private var checkJob: Job? = null + private var originSize = 0 + private var finishCount = 0 + + private val notificationBuilder by lazy { + NotificationCompat.Builder(this, AppConst.channelIdReadAloud) + .setSmallIcon(R.drawable.ic_network_check) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setContentTitle(getString(R.string.check_book_source)) + .setContentIntent( + activityPendingIntent("activity") + ) + .addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + servicePendingIntent(IntentAction.stop) + ) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.start -> IntentData.get>("checkSourceSelectedIds")?.let { + check(it) + } + + IntentAction.resume -> upNotification() + IntentAction.stop -> stopSelf() + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + super.onDestroy() + Debug.finishChecking() + searchCoroutine.close() + postEvent(EventBus.CHECK_SOURCE_DONE, 0) + notificationManager.cancel(NotificationId.CheckSourceService) + } + + private fun check(ids: List) { + if (checkJob?.isActive == true) { + toastOnUi("已有书源在校验,等完成后再试") + return + } + checkJob = lifecycleScope.launch(searchCoroutine) { + flow { + for (origin in ids) { + appDb.bookSourceDao.getBookSource(origin)?.let { + emit(it) + } + } + }.onStart { + originSize = ids.size + finishCount = 0 + notificationMsg = getString(R.string.progress_show, "", 0, originSize) + upNotification() + }.onEachParallel(threadCount) { + checkSource(it) + }.onEach { + finishCount++ + notificationMsg = getString( + R.string.progress_show, + it.bookSourceName, + finishCount, + originSize + ) + upNotification() + appDb.bookSourceDao.update(it) + }.onCompletion { + stopSelf() + }.collect() + } + } + + private suspend fun checkSource(source: BookSource) { + kotlin.runCatching { + withTimeout(CheckSource.timeout) { + doCheckSource(source) + } + }.onSuccess { + Debug.updateFinalMessage(source.bookSourceUrl, "校验成功") + }.onFailure { + coroutineContext.ensureActive() + when (it) { + is TimeoutCancellationException -> source.addGroup("校验超时") + is ScriptException, is WrappedException -> source.addGroup("js失效") + !is NoStackTraceException -> source.addGroup("网站失效") + } + source.addErrorComment(it) + Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}") + } + source.respondTime = Debug.getRespondTime(source.bookSourceUrl) + } + + private suspend fun doCheckSource(source: BookSource) { + Debug.startChecking(source) + source.removeInvalidGroups() + source.removeErrorComment() + //校验搜索书籍 + if (CheckSource.checkSearch) { + val searchWord = source.getCheckKeyword(CheckSource.keyword) + if (!source.searchUrl.isNullOrBlank()) { + source.removeGroup("搜索链接规则为空") + val searchBooks = WebBook.searchBookAwait(source, searchWord) + if (searchBooks.isEmpty()) { + source.addGroup("搜索失效") + } else { + source.removeGroup("搜索失效") + checkBook(searchBooks.first().toBook(), source) + } + } else { + source.addGroup("搜索链接规则为空") + } + } + //校验发现书籍 + if (CheckSource.checkDiscovery && !source.exploreUrl.isNullOrBlank()) { + val url = source.exploreKinds().firstOrNull { + !it.url.isNullOrBlank() + }?.url + if (url.isNullOrBlank()) { + source.addGroup("发现规则为空") + } else { + source.removeGroup("发现规则为空") + val exploreBooks = WebBook.exploreBookAwait(source, url) + if (exploreBooks.isEmpty()) { + source.addGroup("发现失效") + } else { + source.removeGroup("发现失效") + checkBook(exploreBooks.first().toBook(), source, false) + } + } + } + val finalCheckMessage = source.getInvalidGroupNames() + if (finalCheckMessage.isNotBlank()) { + throw NoStackTraceException(finalCheckMessage) + } + } + + /** + *校验书源的详情目录正文 + */ + private suspend fun checkBook(book: Book, source: BookSource, isSearchBook: Boolean = true) { + kotlin.runCatching { + if (!CheckSource.checkInfo) { + return + } + //校验详情 + if (book.tocUrl.isBlank()) { + WebBook.getBookInfoAwait(source, book) + } + if (!CheckSource.checkCategory || source.bookSourceType == BookSourceType.file) { + return + } + //校验目录 + val toc = WebBook.getChapterListAwait(source, book).getOrThrow().asSequence() + .filter { !(it.isVolume && it.url.startsWith(it.title)) } + .take(2) + .toList() + val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url + if (!CheckSource.checkContent) { + return + } + //校验正文 + WebBook.getContentAwait( + bookSource = source, + book = book, + bookChapter = toc.first(), + nextChapterUrl = nextChapterUrl, + needSave = false + ) + }.onFailure { + val bookType = if (isSearchBook) "搜索" else "发现" + when (it) { + is ContentEmptyException -> source.addGroup("${bookType}正文失效") + is TocEmptyException -> source.addGroup("${bookType}目录失效") + else -> throw it + } + }.onSuccess { + val bookType = if (isSearchBook) "搜索" else "发现" + source.removeGroup("${bookType}目录失效") + source.removeGroup("${bookType}正文失效") + } + } + + private fun upNotification() { + notificationBuilder.setContentText(notificationMsg) + notificationBuilder.setProgress(originSize, finishCount, false) + postEvent(EventBus.CHECK_SOURCE, notificationMsg) + notificationManager.notify(NotificationId.CheckSourceService, notificationBuilder.build()) + } + + /** + * 更新通知 + */ + override fun startForegroundNotification() { + notificationBuilder.setContentText(notificationMsg) + notificationBuilder.setProgress(originSize, finishCount, false) + postEvent(EventBus.CHECK_SOURCE, notificationMsg) + startForeground(NotificationId.CheckSourceService, notificationBuilder.build()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/DownloadService.kt b/app/src/main/java/io/legado/app/service/DownloadService.kt new file mode 100755 index 000000000..be9173144 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -0,0 +1,279 @@ +package io.legado.app.service + +import android.annotation.SuppressLint +import android.app.DownloadManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Environment +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.utils.IntentType +import io.legado.app.utils.openFileUri +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import splitties.init.appCtx +import splitties.systemservices.downloadManager +import splitties.systemservices.notificationManager + +/** + * 下载文件 + */ +class DownloadService : BaseService() { + private val groupKey = "${appCtx.packageName}.download" + private val downloads = hashMapOf() + private val completeDownloads = hashSetOf() + private var upStateJob: Job? = null + private val downloadReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + queryState() + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onCreate() { + super.onCreate() + ContextCompat.registerReceiver( + this, + downloadReceiver, + IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), + ContextCompat.RECEIVER_EXPORTED + ) + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(downloadReceiver) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.start -> startDownload( + intent.getStringExtra("url"), + intent.getStringExtra("fileName") + ) + + IntentAction.play -> { + val id = intent.getLongExtra("downloadId", 0) + if (completeDownloads.contains(id)) { + openDownload(id, downloads[id]?.fileName) + } else { + toastOnUi("未完成,下载的文件夹Download") + } + } + + IntentAction.stop -> { + val downloadId = intent.getLongExtra("downloadId", 0) + removeDownload(downloadId) + } + } + return super.onStartCommand(intent, flags, startId) + } + + /** + * 开始下载 + */ + @Synchronized + private fun startDownload(url: String?, fileName: String?) { + if (url == null || fileName == null) { + if (downloads.isEmpty()) { + stopSelf() + } + return + } + if (downloads.values.any { it.url == url }) { + toastOnUi("已在下载列表") + return + } + kotlin.runCatching { + // 指定下载地址 + val request = DownloadManager.Request(Uri.parse(url)) + // 设置通知 + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) + // 设置下载文件保存的路径和文件名 + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName) + // 添加一个下载任务 + val downloadId = downloadManager.enqueue(request) + downloads[downloadId] = + DownloadInfo(url, fileName, NotificationId.Download + downloads.size) + queryState() + if (upStateJob == null) { + checkDownloadState() + } + }.onFailure { + it.printStackTrace() + val msg = when (it) { + is SecurityException -> "下载出错,没有存储权限" + else -> "下载出错,${it.localizedMessage}" + } + toastOnUi(msg) + AppLog.put(msg, it) + } + } + + /** + * 取消下载 + */ + @Synchronized + private fun removeDownload(downloadId: Long) { + if (!completeDownloads.contains(downloadId)) { + downloadManager.remove(downloadId) + } + downloads.remove(downloadId) + completeDownloads.remove(downloadId) + notificationManager.cancel(downloadId.toInt()) + } + + /** + * 下载成功 + */ + @Synchronized + private fun successDownload(downloadId: Long) { + if (!completeDownloads.contains(downloadId)) { + completeDownloads.add(downloadId) + val fileName = downloads[downloadId]?.fileName + openDownload(downloadId, fileName) + } + } + + private fun checkDownloadState() { + upStateJob?.cancel() + upStateJob = lifecycleScope.launch { + while (isActive) { + queryState() + delay(1000) + } + } + } + + /** + * 查询下载进度 + */ + @Synchronized + private fun queryState() { + if (downloads.isEmpty()) { + stopSelf() + return + } + val ids = downloads.keys + val query = DownloadManager.Query() + query.setFilterById(*ids.toLongArray()) + downloadManager.query(query).use { cursor -> + if (cursor.moveToFirst()) { + val idIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID) + val progressIndex = + cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + val fileSizeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS) + do { + val id = cursor.getLong(idIndex) + val progress = cursor.getInt(progressIndex) + val max = cursor.getInt(fileSizeIndex) + val status = when (cursor.getInt(statusIndex)) { + DownloadManager.STATUS_PAUSED -> getString(R.string.pause) + DownloadManager.STATUS_PENDING -> getString(R.string.wait_download) + DownloadManager.STATUS_RUNNING -> getString(R.string.downloading) + DownloadManager.STATUS_SUCCESSFUL -> { + successDownload(id) + getString(R.string.download_success) + } + + DownloadManager.STATUS_FAILED -> getString(R.string.download_error) + else -> getString(R.string.unknown_state) + } + downloads[id]?.let { downloadInfo -> + upDownloadNotification( + id, + downloadInfo.notificationId, + "${downloadInfo.fileName} $status", + max, + progress, + downloadInfo.startTime + ) + } + } while (cursor.moveToNext()) + } + } + } + + /** + * 打开下载文件 + */ + private fun openDownload(downloadId: Long, fileName: String?) { + kotlin.runCatching { + downloadManager.getUriForDownloadedFile(downloadId)?.let { uri -> + val type = IntentType.from(fileName) + openFileUri(uri, type) + } + }.onFailure { + AppLog.put("打开下载文件${fileName}出错", it) + } + } + + override fun startForegroundNotification() { + val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_download) + .setSubText(getString(R.string.action_download)) + .setGroup(groupKey) + .setGroupSummary(true) + .setOngoing(true) + .build() + startForeground(NotificationId.DownloadService, notification) + } + + /** + * 更新通知 + */ + private fun upDownloadNotification( + downloadId: Long, + notificationId: Int, + content: String, + max: Int, + progress: Int, + startTime: Long + ) { + val notificationBuilder = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_download) + .setSubText(getString(R.string.action_download)) + .setContentTitle(content) + .setOnlyAlertOnce(true) + .setContentIntent( + servicePendingIntent(IntentAction.play, downloadId.toInt()) { + putExtra("downloadId", downloadId) + } + ) + .setDeleteIntent( + servicePendingIntent(IntentAction.stop, downloadId.toInt()) { + putExtra("downloadId", downloadId) + } + ) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setGroup(groupKey) + .setWhen(startTime) + if (progress < max) { + notificationBuilder.setProgress(max, progress, false) + } + notificationManager.notify(notificationId, notificationBuilder.build()) + } + + private data class DownloadInfo( + val url: String, + val fileName: String, + val notificationId: Int, + val startTime: Long = System.currentTimeMillis() + ) + +} diff --git a/app/src/main/java/io/legado/app/service/ExportBookService.kt b/app/src/main/java/io/legado/app/service/ExportBookService.kt new file mode 100755 index 000000000..c94873036 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/ExportBookService.kt @@ -0,0 +1,878 @@ +package io.legado.app.service + +import android.annotation.SuppressLint +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.getExportFileName +import io.legado.app.help.book.isLocalModified +import io.legado.app.help.config.AppConfig +import io.legado.app.model.ReadBook +import io.legado.app.model.localBook.LocalBook +import io.legado.app.ui.book.cache.CacheActivity +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.activityPendingIntent +import io.legado.app.utils.cnCompare +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.delete +import io.legado.app.utils.find +import io.legado.app.utils.list +import io.legado.app.utils.mapAsync +import io.legado.app.utils.mapAsyncIndexed +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.postEvent +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeFile +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.collectIndexed +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import me.ag2s.epublib.domain.Author +import me.ag2s.epublib.domain.Date +import me.ag2s.epublib.domain.EpubBook +import me.ag2s.epublib.domain.FileResourceProvider +import me.ag2s.epublib.domain.LazyResource +import me.ag2s.epublib.domain.LazyResourceProvider +import me.ag2s.epublib.domain.Metadata +import me.ag2s.epublib.domain.Resource +import me.ag2s.epublib.domain.TOCReference +import me.ag2s.epublib.epub.EpubWriter +import me.ag2s.epublib.epub.EpubWriterProcessor +import me.ag2s.epublib.util.ResourceUtil +import splitties.init.appCtx +import splitties.systemservices.notificationManager +import java.nio.charset.Charset +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.coroutineContext +import kotlin.math.min + +/** + * 导出书籍服务 + */ +class ExportBookService : BaseService() { + + companion object { + val exportProgress = ConcurrentHashMap() + val exportMsg = ConcurrentHashMap() + } + + data class ExportConfig( + val path: String, + val type: String, + val epubSize: Int = 1, + val epubScope: String? = null + ) + + private val groupKey = "${appCtx.packageName}.exportBook" + private val waitExportBooks = linkedMapOf() + private var exportJob: Job? = null + private var notificationContentText = appCtx.getString(R.string.service_starting) + + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.start -> kotlin.runCatching { + val bookUrl = intent.getStringExtra("bookUrl")!! + if (!exportProgress.contains(bookUrl)) { + val exportConfig = ExportConfig( + path = intent.getStringExtra("exportPath")!!, + type = intent.getStringExtra("exportType")!!, + epubSize = intent.getIntExtra("epubSize", 1), + epubScope = intent.getStringExtra("epubScope") + ) + waitExportBooks[bookUrl] = exportConfig + exportMsg[bookUrl] = getString(R.string.export_wait) + postEvent(EventBus.EXPORT_BOOK, bookUrl) + export() + } + }.onFailure { + toastOnUi(it.localizedMessage) + } + + IntentAction.stop -> { + notificationManager.cancel(NotificationId.ExportBook) + stopSelf() + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + super.onDestroy() + exportProgress.clear() + exportMsg.clear() + waitExportBooks.keys.forEach { + postEvent(EventBus.EXPORT_BOOK, it) + } + } + + @SuppressLint("MissingPermission") + override fun startForegroundNotification() { + val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_export) + .setSubText(getString(R.string.export_book)) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setGroup(groupKey) + .setGroupSummary(true) + startForeground(NotificationId.ExportBookService, notification.build()) + } + + private fun upExportNotification(finish: Boolean = false) { + val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload) + .setSmallIcon(R.drawable.ic_export) + .setSubText(getString(R.string.export_book)) + .setContentIntent(activityPendingIntent("cacheActivity")) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentText(notificationContentText) + .setDeleteIntent(servicePendingIntent(IntentAction.stop)) + .setGroup(groupKey) + .setOnlyAlertOnce(true) + if (!finish) { + notification.setOngoing(true) + notification.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + servicePendingIntent(IntentAction.stop) + ) + } + notificationManager.notify(NotificationId.ExportBook, notification.build()) + } + + private fun export() { + if (exportJob?.isActive == true) { + return + } + exportJob = lifecycleScope.launch(IO) { + while (isActive) { + val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let { + notificationContentText = "导出完成" + upExportNotification(true) + stopSelf() + return@launch + } + exportProgress[bookUrl] = 0 + waitExportBooks.remove(bookUrl) + val book = appDb.bookDao.getBook(bookUrl) + try { + book ?: throw NoStackTraceException("获取${bookUrl}书籍出错") + refreshChapterList(book) + notificationContentText = getString( + R.string.export_book_notification_content, + book.name, + waitExportBooks.size + ) + upExportNotification() + if (exportConfig.type == "epub") { + if (exportConfig.epubScope.isNullOrBlank()) { + exportEpub(exportConfig.path, book) + } else { + CustomExporter( + exportConfig.epubScope, + exportConfig.epubSize + ).export(exportConfig.path, book) + } + } else { + exportTxt(exportConfig.path, book) + } + exportMsg[book.bookUrl] = getString(R.string.export_success) + } catch (e: Throwable) { + ensureActive() + exportMsg[bookUrl] = e.localizedMessage ?: "ERROR" + AppLog.put("导出书籍<${book?.name ?: bookUrl}>出错", e) + } finally { + exportProgress.remove(bookUrl) + postEvent(EventBus.EXPORT_BOOK, bookUrl) + } + } + } + } + + private fun refreshChapterList(book: Book) { + if (!book.isLocalModified()) { + return + } + kotlin.runCatching { + LocalBook.getChapterList(book) + }.onSuccess { + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*it.toTypedArray()) + appDb.bookDao.update(book) + ReadBook.onChapterListUpdated(book) + } + } + + private data class SrcData( + val chapterTitle: String, + val index: Int, + val src: String + ) + + private suspend fun exportTxt(path: String, book: Book) { + exportMsg.remove(book.bookUrl) + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + val fileDoc = FileDoc.fromDir(path) + exportTxt(fileDoc, book) + } + + private suspend fun exportTxt(fileDoc: FileDoc, book: Book) { + val filename = book.getExportFileName("txt") + fileDoc.find(filename)?.delete() + + val bookDoc = fileDoc.createFileIfNotExist(filename) + val charset = Charset.forName(AppConfig.exportCharset) + bookDoc.openOutputStream().getOrThrow().bufferedWriter(charset).use { bw -> + getAllContents(book) { text, srcList -> + bw.write(text) + srcList?.forEach { + val vFile = BookHelp.getImage(book, it.src) + if (vFile.exists()) { + fileDoc.createFileIfNotExist( + "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg", + subDirs = arrayOf( + "${book.name}_${book.author}", + "images", + it.chapterTitle + ) + ).writeFile(vFile) + } + } + } + } + if (AppConfig.exportToWebDav) { + // 导出到webdav + AppWebDav.exportWebDav(bookDoc.uri, filename) + } + } + + private suspend fun getAllContents( + book: Book, + append: (text: String, srcList: ArrayList?) -> Unit + ) = coroutineScope { + val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule() + val contentProcessor = ContentProcessor.get(book.name, book.origin) + val qy = "${book.name}\n${ + getString(R.string.author_show, book.getRealAuthor()) + }\n${ + getString( + R.string.intro_show, + "\n" + HtmlFormatter.format(book.getDisplayIntro()) + ) + }" + append(qy, null) + val threads = if (AppConfig.parallelExportBook) { + AppConst.MAX_THREAD + } else { + 1 + } + flow { + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + emit(chapter) + } + }.mapAsync(threads) { chapter -> + getExportData(book, chapter, contentProcessor, useReplace) + }.collectIndexed { index, result -> + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + exportProgress[book.bookUrl] = index + append.invoke(result.first, result.second) + } + + } + + private fun getExportData( + book: Book, + chapter: BookChapter, + contentProcessor: ContentProcessor, + useReplace: Boolean + ): Pair?> { + val content = BookHelp.getContent(book, chapter) + val content1 = contentProcessor + .getContent( + book, + // 不导出vip标识 + chapter.apply { isVip = false }, + content ?: if (chapter.isVolume) "" else "null", + includeTitle = !AppConfig.exportNoChapterName, + useReplace = useReplace, + chineseConvert = false, + reSegment = false + ).toString() + if (AppConfig.exportPictureFile) { + //txt导出图片文件 + val srcList = arrayListOf() + content?.split("\n")?.forEachIndexed { index, text -> + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { + val src = NetworkUtils.getAbsoluteURL(chapter.url, it) + srcList.add(SrcData(chapter.title, index, src)) + } + } + } + return Pair("\n\n$content1", srcList) + } else { + return Pair("\n\n$content1", null) + } + } + + /** + * 导出Epub + */ + private suspend fun exportEpub(path: String, book: Book) { + exportMsg.remove(book.bookUrl) + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + val fileDoc = FileDoc.fromDir(path) + exportEpub(fileDoc, book) + } + + private suspend fun exportEpub(fileDoc: FileDoc, book: Book) { + val filename = book.getExportFileName("epub") + fileDoc.find(filename)?.delete() + + val epubBook = EpubBook() + epubBook.version = "2.0" + //set metadata + setEpubMetadata(book, epubBook) + //set cover + setCover(book, epubBook) + //set css + val contentModel = setAssets(fileDoc, book, epubBook) + + //设置正文 + setEpubContent(contentModel, book, epubBook) + + val bookDoc = fileDoc.createFileIfNotExist(filename) + bookDoc.openOutputStream().getOrThrow().buffered().use { bookOs -> + EpubWriter().write(epubBook, bookOs) + } + + if (AppConfig.exportToWebDav) { + // 导出到webdav + AppWebDav.exportWebDav(bookDoc.uri, filename) + } + } + + private fun setAssets(doc: FileDoc, book: Book, epubBook: EpubBook): String { + val customPath = doc.find("Asset") + val contentModel = if (customPath == null) {//使用内置模板 + setAssets(book, epubBook) + } else {//外部模板 + setAssetsExternal(customPath, book, epubBook) + } + + return contentModel + } + + private fun setAssetsExternal(doc: FileDoc, book: Book, epubBook: EpubBook): String { + var contentModel = "" + doc.list()!!.forEach { folder -> + if (folder.isDir && folder.name == "Text") { + folder.list()!!.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + }.forEach loop@{ file -> + if (file.isDir) { + return@loop + } + when { + //正文模板 + file.name.equals("chapter.html", true) + || file.name.equals("chapter.xhtml", true) -> { + contentModel = file.readText() + } + //封面等其他模板 + file.name.endsWith("html", true) -> { + epubBook.addSection( + FileUtils.getNameExcludeExtension(file.name), + ResourceUtil.createPublicResource( + book.name, + book.getRealAuthor(), + book.getDisplayIntro(), + book.kind, + book.wordCount, + file.readText(), + "${folder.name}/${file.name}" + ) + ) + } + //其他格式文件当做资源文件 + else -> { + epubBook.resources.add( + Resource( + file.readBytes(), + "${folder.name}/${file.name}" + ) + ) + } + } + } + } else if (folder.isDir) { + //资源文件 + folder.list()!!.forEach loop2@{ + if (it.isDir) { + return@loop2 + } + epubBook.resources.add( + Resource( + it.readBytes(), + "${folder.name}/${it.name}" + ) + ) + } + } else {//Asset下面的资源文件 + epubBook.resources.add( + Resource( + folder.readBytes(), + folder.name + ) + ) + } + } + return contentModel + } + + private fun setAssets(book: Book, epubBook: EpubBook): String { + epubBook.resources.add( + Resource( + appCtx.assets.open("epub/fonts.css").readBytes(), + "Styles/fonts.css" + ) + ) + epubBook.resources.add( + Resource( + appCtx.assets.open("epub/main.css").readBytes(), + "Styles/main.css" + ) + ) + epubBook.resources.add( + Resource( + appCtx.assets.open("epub/logo.png").readBytes(), + "Images/logo.png" + ) + ) + epubBook.addSection( + getString(R.string.img_cover), + ResourceUtil.createPublicResource( + book.name, + book.getRealAuthor(), + book.getDisplayIntro(), + book.kind, + book.wordCount, + String(appCtx.assets.open("epub/cover.html").readBytes()), + "Text/cover.html" + ) + ) + epubBook.addSection( + getString(R.string.book_intro), + ResourceUtil.createPublicResource( + book.name, + book.getRealAuthor(), + book.getDisplayIntro(), + book.kind, + book.wordCount, + String(appCtx.assets.open("epub/intro.html").readBytes()), + "Text/intro.html" + ) + ) + return String(appCtx.assets.open("epub/chapter.html").readBytes()) + } + + private fun setCover(book: Book, epubBook: EpubBook) { + kotlin.runCatching { + val file = Glide.with(this) + .asFile() + .load(book.getDisplayCover()) + .submit() + .get() + val provider = LazyResourceProvider { _ -> + file.inputStream() + } + epubBook.coverImage = LazyResource(provider, "Images/cover.jpg") + }.onFailure { + AppLog.put("获取书籍封面出错\n${it.localizedMessage}", it) + } + } + + private suspend fun setEpubContent( + contentModel: String, + book: Book, + epubBook: EpubBook + ) = coroutineScope { + //正文 + val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule() + val contentProcessor = ContentProcessor.get(book.name, book.origin) + val threads = if (AppConfig.parallelExportBook) { + AppConst.MAX_THREAD + } else { + 1 + } + var parentSection: TOCReference? = null + flow { + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + emit(chapter) + } + }.mapAsyncIndexed(threads) { index, chapter -> + val content = BookHelp.getContent(book, chapter) + val (contentFix, resources) = fixPic( + book, + content ?: if (chapter.isVolume) "" else "null", + chapter + ) + // 不导出vip标识 + chapter.isVip = false + val content1 = contentProcessor + .getContent( + book, + chapter, + contentFix, + includeTitle = false, + useReplace = useReplace, + chineseConvert = false, + reSegment = false + ).toString() + val title = chapter.run { + // 不导出vip标识 + isVip = false + getDisplayTitle( + contentProcessor.getTitleReplaceRules(), + useReplace = useReplace + ) + } + val chapterResource = ResourceUtil.createChapterResource( + title.replace("\uD83D\uDD12", ""), + content1, + contentModel, + "Text/chapter_${index}.html" + ) + ExportChapter(title, chapterResource, resources, chapter) + }.collectIndexed { index, exportChapter -> + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + exportProgress[book.bookUrl] = index + val (title, chapterResource, resources, chapter) = exportChapter + epubBook.resources.addAll(resources) + if (chapter.isVolume) { + parentSection = epubBook.addSection(title, chapterResource) + } else if (parentSection == null) { + epubBook.addSection(title, chapterResource) + } else { + epubBook.addSection(parentSection, title, chapterResource) + } + } + } + + data class ExportChapter( + val title: String, + val chapterResource: Resource, + val resources: ArrayList, + val chapter: BookChapter + ) + + private fun fixPic( + book: Book, + content: String, + chapter: BookChapter + ): Pair> { + val data = StringBuilder("") + val resources = arrayListOf() + content.split("\n").forEach { text -> + var text1 = text + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { + val src = NetworkUtils.getAbsoluteURL(chapter.url, it) + val originalHref = + "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" + val href = + "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" + val vFile = BookHelp.getImage(book, src) + val fp = FileResourceProvider(vFile.parent) + if (vFile.exists()) { + val img = LazyResource(fp, href, originalHref) + resources.add(img) + } + text1 = text1.replace(src, "../${href}") + } + } + data.append(text1).append("\n") + } + return data.toString() to resources + } + + private fun setEpubMetadata(book: Book, epubBook: EpubBook) { + val metadata = Metadata() + metadata.titles.add(book.name)//书籍的名称 + metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者 + metadata.language = "zh"//数据的语言 + metadata.dates.add(Date())//数据的创建日期 + metadata.publishers.add("Legado")//数据的创建者 + metadata.descriptions.add(book.getDisplayIntro())//书籍的简介 + //metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍 + epubBook.metadata = metadata + } + + //////end of EPUB + + //////start of custom exporter + /** + * 自定义Exporter + * @param scope 导出范围 + * @param size epub 文件包含最大章节数 + */ + inner class CustomExporter(scopeStr: String, private val size: Int) { + + private var scope = parseScope(scopeStr) + + /** + * 导出Epub + * @param path 导出的路径 + * @param book 书籍 + */ + suspend fun export( + path: String, + book: Book + ) { + exportProgress[book.bookUrl] = 0 + exportMsg.remove(book.bookUrl) + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + val currentTimeMillis = System.currentTimeMillis() + val count = appDb.bookChapterDao.getChapterCount(book.bookUrl) + scope = scope.filter { it < count }.toHashSet() + + val fileDoc = FileDoc.fromDir(path) + + val (contentModel, epubList) = createEpubs(book, fileDoc) + var progressBar = 0.0 + epubList.forEachIndexed { index, ep -> + val (filename, epubBook) = ep + //设置正文 + setEpubContent( + contentModel, + book, + epubBook, + index + ) { _, _ -> + // 将章节写入内存时更新进度条 + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + progressBar += book.totalChapterNum.toDouble() / scope.size / 2 + exportProgress[book.bookUrl] = progressBar.toInt() + } + save2Drive(filename, epubBook, fileDoc) { total, _ -> + //写入硬盘时更新进度条 + progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2 + postEvent(EventBus.EXPORT_BOOK, book.bookUrl) + exportProgress[book.bookUrl] = progressBar.toInt() + } + } + + val elapsed = System.currentTimeMillis() - currentTimeMillis + AppLog.put("分割导出书籍 ${book.name} 一共耗时 $elapsed") + } + + + /** + * 设置epub正文 + * + * @param contentModel 正文模板 + * @param book 书籍 + * @param epubBook 分割后的epub + * @param epubBookIndex 分割后的epub序号 + */ + private suspend fun setEpubContent( + contentModel: String, + book: Book, + epubBook: EpubBook, + epubBookIndex: Int, + updateProgress: (chapterList: MutableList, index: Int) -> Unit + ) { + //正文 + val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule() + val contentProcessor = ContentProcessor.get(book.name, book.origin) + var chapterList: MutableList = ArrayList() + appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> + if (scope.contains(index)) { + chapterList.add(chapter) + } + if (scope.size == chapterList.size) { + return@forEachIndexed + } + } + // val totalChapterNum = book.totalChapterNum / scope.size + if (chapterList.isEmpty()) { + throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息") + } + chapterList = chapterList.subList( + epubBookIndex * size, + min(scope.size, (epubBookIndex + 1) * size) + ) + chapterList.forEachIndexed { index, chapter -> + coroutineContext.ensureActive() + updateProgress(chapterList, index) + BookHelp.getContent(book, chapter).let { content -> + val (contentFix, resources) = fixPic( + book, + content ?: if (chapter.isVolume) "" else "null", + chapter + ) + epubBook.resources.addAll(resources) + val content1 = contentProcessor + .getContent( + book, + chapter, + contentFix, + includeTitle = false, + useReplace = useReplace, + chineseConvert = false, + reSegment = false + ).toString() + val title = chapter.run { + // 不导出vip标识 + isVip = false + getDisplayTitle( + contentProcessor.getTitleReplaceRules(), + useReplace = useReplace + ) + } + epubBook.addSection( + title, + ResourceUtil.createChapterResource( + title.replace("\uD83D\uDD12", ""), + content1, + contentModel, + "Text/chapter_${index}.html" + ) + ) + } + } + } + + /** + * 创建多个epub 对象 + * + * 分割epub时,一个书籍需要创建多个epub对象 + * @param book 书籍 + * @param fileDoc 导出文件夹文档 + * + * @return <内容模板字符串, > + */ + private fun createEpubs( + book: Book, + fileDoc: FileDoc + ): Pair>> { + val paresNumOfEpub = paresNumOfEpub(scope.size, size) + val result: MutableList> = ArrayList(paresNumOfEpub) + var contentModel = "" + for (i in 1..paresNumOfEpub) { + val filename = book.getExportFileName("epub", i) + fileDoc.find(filename)?.delete() + + val epubBook = EpubBook() + epubBook.version = "2.0" + //set metadata + setEpubMetadata(book, epubBook) + //set cover + setCover(book, epubBook) + //set css + contentModel = setAssets(fileDoc, book, epubBook) + + // add epubBook + result.add(Pair(filename, epubBook)) + } + return Pair(contentModel, result) + } + + /** + * 保存文件到 设备 + */ + private suspend fun save2Drive( + filename: String, + epubBook: EpubBook, + fileDoc: FileDoc, + callback: (total: Int, progress: Int) -> Unit + ) { + val bookDoc = fileDoc.createFileIfNotExist(filename) + bookDoc.openOutputStream().getOrThrow().buffered().use { bookOs -> + EpubWriter() + .setCallback(object : EpubWriterProcessor.Callback { + override fun onProgressing(total: Int, progress: Int) { + callback(total, progress) + } + }) + .write(epubBook, bookOs) + } + + if (AppConfig.exportToWebDav) { + // 导出到webdav + AppWebDav.exportWebDav(bookDoc.uri, filename) + } + } + + /** + * 解析 分割epub后的数量 + * + * @param total 章节总数 + * @param size 每个epub文件包含多少章节 + */ + private fun paresNumOfEpub(total: Int, size: Int): Int { + val i = total % size + var result = total / size + if (i > 0) { + result++ + } + return result + } + + /** + * 解析范围字符串 + * + * @param scope 范围字符串 + * @return 范围 + * + * @since 2023/5/22 + * @author Discut + */ + private fun parseScope(scope: String): Set { + val split = scope.split(",") + + val result = linkedSetOf() + for (s in split) { + val v = s.split("-") + if (v.size != 2) { + result.add(s.toInt() - 1) + continue + } + val left = v[0].toInt() + val right = v[1].toInt() + if (left > right) { + AppLog.put("Error expression : $s; left > right") + continue + } + for (i in left..right) + result.add(i - 1) + } + return result + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt new file mode 100755 index 000000000..9989bd232 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -0,0 +1,596 @@ +package io.legado.app.service + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.Timeline +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.cache.CacheDataSink +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.offline.DefaultDownloaderFactory +import androidx.media3.exoplayer.offline.DownloadRequest +import androidx.media3.exoplayer.offline.Downloader +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy +import com.script.ScriptException +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.HttpTTS +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.exoplayer.InputStreamDataSource +import io.legado.app.help.http.okHttpClient +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.Response +import org.mozilla.javascript.WrappedException +import splitties.init.appCtx +import java.io.File +import java.io.InputStream +import java.net.ConnectException +import java.net.SocketTimeoutException + +/** + * 在线朗读 + */ +@SuppressLint("UnsafeOptInUsageError") +class HttpReadAloudService : BaseReadAloudService(), + Player.Listener { + private val exoPlayer: ExoPlayer by lazy { + ExoPlayer.Builder(this).build() + } + private val ttsFolderPath: String by lazy { + cacheDir.absolutePath + File.separator + "httpTTS" + File.separator + } + private val cache by lazy { + SimpleCache( + File(cacheDir, "httpTTS_cache"), + LeastRecentlyUsedCacheEvictor(128 * 1024 * 1024), + StandaloneDatabaseProvider(appCtx) + ) + } + private val cacheDataSinkFactory by lazy { + CacheDataSink.Factory() + .setCache(cache) + } + private val loadErrorHandlingPolicy by lazy { + CustomLoadErrorHandlingPolicy() + } + private var speechRate: Int = AppConfig.speechRatePlay + 5 + private var downloadTask: Coroutine<*>? = null + private var playIndexJob: Job? = null + private var downloadErrorNo: Int = 0 + private var playErrorNo = 0 + private val downloadTaskActiveLock = Mutex() + + override fun onCreate() { + super.onCreate() + exoPlayer.addListener(this) + } + + override fun onDestroy() { + super.onDestroy() + downloadTask?.cancel() + exoPlayer.release() + cache.release() + Coroutine.async { + removeCacheFile() + } + } + + override fun play() { + pageChanged = false + exoPlayer.stop() + if (!requestFocus()) return + if (contentList.isEmpty()) { + AppLog.putDebug("朗读列表为空") + ReadBook.readAloud() + } else { + super.play() + if (AppConfig.streamReadAloudAudio) { + downloadAndPlayAudiosStream() + } else { + downloadAndPlayAudios() + } + } + } + + override fun playStop() { + exoPlayer.stop() + playIndexJob?.cancel() + } + + private fun updateNextPos() { + readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos + paragraphStartPos = 0 + if (nowSpeak < contentList.lastIndex) { + nowSpeak++ + } else { + nextChapter() + } + } + + private fun downloadAndPlayAudios() { + exoPlayer.clearMediaItems() + downloadTask?.cancel() + downloadTask = execute { + downloadTaskActiveLock.withLock { + ensureActive() + val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null") + contentList.forEachIndexed { index, content -> + ensureActive() + if (index < nowSpeak) return@forEachIndexed + var text = content + if (paragraphStartPos > 0 && index == nowSpeak) { + text = text.substring(paragraphStartPos) + } + val fileName = md5SpeakFileName(text) + val speakText = text.replace(AppPattern.notReadAloudRegex, "") + if (speakText.isEmpty()) { + AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$text") + createSilentSound(fileName) + } else if (!hasSpeakFile(fileName)) { + runCatching { + val inputStream = getSpeakStream(httpTts, speakText) + if (inputStream != null) { + createSpeakFile(fileName, inputStream) + } else { + createSilentSound(fileName) + } + }.onFailure { + when (it) { + is CancellationException -> Unit + else -> pauseReadAloud() + } + return@execute + } + } + val file = getSpeakFileAsMd5(fileName) + val mediaItem = MediaItem.fromUri(Uri.fromFile(file)) + launch(Main) { + exoPlayer.addMediaItem(mediaItem) + } + } + preDownloadAudios(httpTts) + } + }.onError { + AppLog.put("朗读下载出错\n${it.localizedMessage}", it, true) + } + } + + private suspend fun preDownloadAudios(httpTts: HttpTTS) { + val textChapter = ReadBook.nextTextChapter ?: return + val contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0, 1) + .splitToSequence("\n") + .filter { it.isNotEmpty() } + .take(10) + .toList() + contentList.forEach { content -> + currentCoroutineContext().ensureActive() + val fileName = md5SpeakFileName(content, textChapter) + val speakText = content.replace(AppPattern.notReadAloudRegex, "") + if (speakText.isEmpty()) { + createSilentSound(fileName) + } else if (!hasSpeakFile(fileName)) { + runCatching { + val inputStream = getSpeakStream(httpTts, speakText) + if (inputStream != null) { + createSpeakFile(fileName, inputStream) + } else { + createSilentSound(fileName) + } + } + } + } + } + + private fun downloadAndPlayAudiosStream() { + exoPlayer.clearMediaItems() + downloadTask?.cancel() + downloadTask = execute { + downloadTaskActiveLock.withLock { + ensureActive() + val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null") + val downloaderChannel = Channel() + launch { + for (downloader in downloaderChannel) { + downloader.download(null) + } + } + contentList.forEachIndexed { index, content -> + ensureActive() + if (index < nowSpeak) return@forEachIndexed + var text = content + if (paragraphStartPos > 0 && index == nowSpeak) { + text = text.substring(paragraphStartPos) + } + val speakText = text.replace(AppPattern.notReadAloudRegex, "") + if (speakText.isEmpty()) { + AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$speakText") + } + val fileName = md5SpeakFileName(text) + val dataSourceFactory = createDataSourceFactory(httpTts, speakText) + val downloader = createDownloader(dataSourceFactory, fileName) + downloaderChannel.send(downloader) + val mediaSource = createMediaSource(dataSourceFactory, fileName) + launch(Main) { + exoPlayer.addMediaSource(mediaSource) + } + } + preDownloadAudiosStream(httpTts, downloaderChannel) + } + }.onError { + AppLog.put("朗读下载出错\n${it.localizedMessage}", it, true) + } + } + + private suspend fun preDownloadAudiosStream( + httpTts: HttpTTS, + downloaderChannel: Channel + ) { + val textChapter = ReadBook.nextTextChapter ?: return + val contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0, 1) + .splitToSequence("\n") + .filter { it.isNotEmpty() } + .take(10) + .toList() + contentList.forEach { content -> + currentCoroutineContext().ensureActive() + val fileName = md5SpeakFileName(content, textChapter) + val speakText = content.replace(AppPattern.notReadAloudRegex, "") + val dataSourceFactory = createDataSourceFactory(httpTts, speakText) + val downloader = createDownloader(dataSourceFactory, fileName) + downloaderChannel.send(downloader) + } + } + + private fun createDataSourceFactory( + httpTts: HttpTTS, + speakText: String + ): CacheDataSource.Factory { + val upstreamFactory = DataSource.Factory { + InputStreamDataSource { + if (speakText.isEmpty()) { + null + } else { + kotlin.runCatching { + runBlocking(lifecycleScope.coroutineContext[Job]!!) { + getSpeakStream(httpTts, speakText) + } + }.onFailure { + when (it) { + is InterruptedException, + is CancellationException -> Unit + + else -> pauseReadAloud() + } + }.getOrThrow() + } ?: resources.openRawResource(R.raw.silent_sound) + } + } + val factory = CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory) + .setCacheWriteDataSinkFactory(cacheDataSinkFactory) + return factory + } + + private fun createDownloader(factory: CacheDataSource.Factory, fileName: String): Downloader { + val uri = fileName.toUri() + val request = DownloadRequest.Builder(fileName, uri).build() + return DefaultDownloaderFactory(factory, okHttpClient.dispatcher.executorService) + .createDownloader(request) + } + + private fun createMediaSource(factory: DataSource.Factory, fileName: String): MediaSource { + return DefaultMediaSourceFactory(this) + .setDataSourceFactory(factory) + .setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) + .createMediaSource(MediaItem.fromUri(fileName)) + } + + private suspend fun getSpeakStream( + httpTts: HttpTTS, + speakText: String + ): InputStream? { + while (true) { + try { + val analyzeUrl = AnalyzeUrl( + httpTts.url, + speakText = speakText, + speakSpeed = speechRate, + source = httpTts, + readTimeout = 300 * 1000L, + coroutineContext = currentCoroutineContext() + ) + var response = analyzeUrl.getResponseAwait() + currentCoroutineContext().ensureActive() + val checkJs = httpTts.loginCheckJs + if (checkJs?.isNotBlank() == true) { + response = analyzeUrl.evalJS(checkJs, response) as Response + } + response.headers["Content-Type"]?.let { contentType -> + val contentType = contentType.substringBefore(";") + val ct = httpTts.contentType + if (contentType == "application/json" || contentType.startsWith("text/")) { + throw NoStackTraceException(response.body.string()) + } else if (ct?.isNotBlank() == true) { + if (!contentType.matches(ct.toRegex())) { + throw NoStackTraceException( + "TTS服务器返回错误:" + response.body.string() + ) + } + } + } + currentCoroutineContext().ensureActive() + response.body.byteStream().let { stream -> + downloadErrorNo = 0 + return stream + } + } catch (e: Exception) { + when (e) { + is CancellationException -> throw e + is ScriptException, is WrappedException -> { + AppLog.put("js错误\n${e.localizedMessage}", e, true) + e.printOnDebug() + throw e + } + + is SocketTimeoutException, is ConnectException -> { + downloadErrorNo++ + if (downloadErrorNo > 5) { + val msg = "tts超时或连接错误超过5次\n${e.localizedMessage}" + AppLog.put(msg, e, true) + throw e + } + } + + else -> { + downloadErrorNo++ + val msg = "tts下载错误\n${e.localizedMessage}" + AppLog.put(msg, e) + e.printOnDebug() + if (downloadErrorNo > 5) { + val msg1 = "TTS服务器连续5次错误,已暂停阅读。" + AppLog.put(msg1, e, true) + throw e + } else { + AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$speakText") + break + } + } + } + } + } + return null + } + + private fun md5SpeakFileName(content: String, textChapter: TextChapter? = this.textChapter): String { + return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" + + MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content") + } + + private fun createSilentSound(fileName: String) { + val file = createSpeakFile(fileName) + file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes()) + } + + private fun hasSpeakFile(name: String): Boolean { + return FileUtils.exist("${ttsFolderPath}$name.mp3") + } + + private fun getSpeakFileAsMd5(name: String): File { + return File("${ttsFolderPath}$name.mp3") + } + + private fun createSpeakFile(name: String): File { + return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3") + } + + private fun createSpeakFile(name: String, inputStream: InputStream) { + FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3").outputStream().use { out -> + inputStream.use { + it.copyTo(out) + } + } + } + + /** + * 移除缓存文件 + */ + private fun removeCacheFile() { + val titleMd5 = MD5Utils.md5Encode16(textChapter?.title ?: "") + FileUtils.listDirsAndFiles(ttsFolderPath)?.forEach { + val isSilentSound = it.length() == 2160L + if ((!it.name.startsWith(titleMd5) + && System.currentTimeMillis() - it.lastModified() > 600000) + || isSilentSound + ) { + FileUtils.delete(it.absolutePath) + } + } + } + + + override fun pauseReadAloud(abandonFocus: Boolean) { + super.pauseReadAloud(abandonFocus) + kotlin.runCatching { + playIndexJob?.cancel() + exoPlayer.pause() + } + } + + override fun resumeReadAloud() { + super.resumeReadAloud() + kotlin.runCatching { + if (pageChanged) { + play() + } else { + exoPlayer.play() + upPlayPos() + } + } + } + + private fun upPlayPos() { + playIndexJob?.cancel() + val textChapter = textChapter ?: return + playIndexJob = lifecycleScope.launch { + upTtsProgress(readAloudNumber + 1) + if (exoPlayer.duration <= 0) { + return@launch + } + val speakTextLength = contentList[nowSpeak].length + if (speakTextLength <= 0) { + return@launch + } + val sleep = exoPlayer.duration / speakTextLength + val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration + for (i in start..contentList[nowSpeak].length) { + if (pageIndex + 1 < textChapter.pageSize + && readAloudNumber + i > textChapter.getReadLength(pageIndex + 1) + ) { + pageIndex++ + ReadBook.moveToNextPage() + upTtsProgress(readAloudNumber + i.toInt()) + } + delay(sleep) + } + } + } + + /** + * 更新朗读速度 + */ + override fun upSpeechRate(reset: Boolean) { + downloadTask?.cancel() + exoPlayer.stop() + speechRate = AppConfig.speechRatePlay + 5 + if (AppConfig.streamReadAloudAudio) { + downloadAndPlayAudiosStream() + } else { + downloadAndPlayAudios() + } + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + when (playbackState) { + Player.STATE_IDLE -> { + // 空闲 + } + + Player.STATE_BUFFERING -> { + // 缓冲中 + } + + Player.STATE_READY -> { + // 准备好 + if (pause) return + exoPlayer.play() + upPlayPos() + } + + Player.STATE_ENDED -> { + // 结束 + playErrorNo = 0 + updateNextPos() + exoPlayer.stop() + exoPlayer.clearMediaItems() + } + } + } + + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + when (reason) { + Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED -> { + if (!timeline.isEmpty && exoPlayer.playbackState == Player.STATE_IDLE) { + exoPlayer.prepare() + } + } + + else -> {} + } + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { + playErrorNo = 0 + } + updateNextPos() + upPlayPos() + } + + override fun onPlayerError(error: PlaybackException) { + super.onPlayerError(error) + AppLog.put("朗读错误\n${contentList[nowSpeak]}", error) + deleteCurrentSpeakFile() + playErrorNo++ + if (playErrorNo >= 5) { + toastOnUi("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})") + AppLog.put("朗读连续5次错误, 最后一次错误代码(${error.localizedMessage})", error) + pauseReadAloud() + } else { + if (exoPlayer.hasNextMediaItem()) { + exoPlayer.seekToNextMediaItem() + exoPlayer.prepare() + } else { + exoPlayer.clearMediaItems() + updateNextPos() + } + } + } + + private fun deleteCurrentSpeakFile() { + if (AppConfig.streamReadAloudAudio) { + return + } + val mediaItem = exoPlayer.currentMediaItem ?: return + val filePath = mediaItem.localConfiguration!!.uri.path!! + File(filePath).delete() + } + + override fun aloudServicePendingIntent(actionStr: String): PendingIntent? { + return servicePendingIntent(actionStr) + } + + class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) { + override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long { + return C.TIME_UNSET + } + } + +} diff --git a/app/src/main/java/io/legado/app/service/README.md b/app/src/main/java/io/legado/app/service/README.md new file mode 100755 index 000000000..ca6bcbfe1 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/README.md @@ -0,0 +1,8 @@ +# android服务 + +* AudioPlayService 音频播放服务 +* CheckSourceService 书源检测服务 +* DownloadService 缓存服务 +* HttpReadAloudService 在线朗读服务 +* TTSReadAloudService tts朗读服务 +* WebService web服务 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt new file mode 100755 index 000000000..578f8aad1 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -0,0 +1,266 @@ +package io.legado.app.service + +import android.app.PendingIntent +import android.speech.tts.TextToSpeech +import android.speech.tts.UtteranceProgressListener +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.MediaHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.utils.GSON +import io.legado.app.utils.LogUtils +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive + +/** + * 本地朗读 + */ +class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener { + + private var textToSpeech: TextToSpeech? = null + private var ttsInitFinish = false + private val ttsUtteranceListener = TTSUtteranceListener() + private var speakJob: Coroutine<*>? = null + private val TAG = "TTSReadAloudService" + + override fun onCreate() { + super.onCreate() + kotlin.runCatching { + initTts() + }.onFailure { + AppLog.put("${getString(R.string.tts_init_failed)}\n$it", it, true) + } + } + + override fun onDestroy() { + super.onDestroy() + clearTTS() + } + + @Synchronized + private fun initTts() { + ttsInitFinish = false + val engine = GSON.fromJsonObject>(ReadAloud.ttsEngine).getOrNull()?.value + LogUtils.d(TAG, "initTts engine:$engine") + textToSpeech = if (engine.isNullOrBlank()) { + TextToSpeech(this, this) + } else { + TextToSpeech(this, this, engine) + } + upSpeechRate() + } + + @Synchronized + fun clearTTS() { + textToSpeech?.runCatching { + stop() + shutdown() + } + textToSpeech = null + ttsInitFinish = false + } + + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + textToSpeech?.let { + it.setOnUtteranceProgressListener(ttsUtteranceListener) + ttsInitFinish = true + play() + } + } else { + toastOnUi(R.string.tts_init_failed) + } + } + + @Synchronized + override fun play() { + if (!ttsInitFinish) return + if (!requestFocus()) return + if (contentList.isEmpty()) { + AppLog.putDebug("朗读列表为空") + ReadBook.readAloud() + return + } + super.play() + MediaHelp.playSilentSound(this@TTSReadAloudService) + speakJob?.cancel() + speakJob = execute { + LogUtils.d(TAG, "朗读列表大小 ${contentList.size}") + LogUtils.d(TAG, "朗读页数 ${textChapter?.pageSize}") + val tts = textToSpeech ?: throw NoStackTraceException("tts is null") + val contentList = contentList + var isAddedText = false + for (i in nowSpeak until contentList.size) { + ensureActive() + var text = contentList[i] + if (paragraphStartPos > 0 && i == nowSpeak) { + text = text.substring(paragraphStartPos) + } + if (text.matches(AppPattern.notReadAloudRegex)) { + continue + } + if (!isAddedText) { + val result = tts.runCatching { + speak(text, TextToSpeech.QUEUE_FLUSH, null, AppConst.APP_TAG + i) + }.getOrElse { + AppLog.put("tts出错\n${it.localizedMessage}", it, true) + TextToSpeech.ERROR + } + if (result == TextToSpeech.ERROR) { + AppLog.put("tts出错 尝试重新初始化") + clearTTS() + initTts() + return@execute + } + } else { + val result = tts.runCatching { + speak(text, TextToSpeech.QUEUE_ADD, null, AppConst.APP_TAG + i) + }.getOrElse { + AppLog.put("tts出错\n${it.localizedMessage}", it, true) + TextToSpeech.ERROR + } + if (result == TextToSpeech.ERROR) { + AppLog.put("tts朗读出错:$text") + } + } + isAddedText = true + } + LogUtils.d(TAG, "朗读内容添加完成") + if (!isAddedText) { + playStop() + delay(1000) + nextChapter() + } + }.onError { + AppLog.put("tts朗读出错\n${it.localizedMessage}", it, true) + } + } + + override fun playStop() { + textToSpeech?.runCatching { + stop() + } + } + + /** + * 更新朗读速度 + */ + override fun upSpeechRate(reset: Boolean) { + if (AppConfig.ttsFlowSys) { + if (reset) { + clearTTS() + initTts() + } + } else { + val speechRate = (AppConfig.ttsSpeechRate + 5) / 10f + textToSpeech?.setSpeechRate(speechRate) + } + } + + /** + * 暂停朗读 + */ + override fun pauseReadAloud(abandonFocus: Boolean) { + super.pauseReadAloud(abandonFocus) + speakJob?.cancel() + textToSpeech?.runCatching { + stop() + } + } + + /** + * 恢复朗读 + */ + override fun resumeReadAloud() { + super.resumeReadAloud() + play() + } + + /** + * 朗读监听 + */ + private inner class TTSUtteranceListener : UtteranceProgressListener() { + + private val TAG = "TTSUtteranceListener" + + override fun onStart(s: String) { + LogUtils.d(TAG, "onStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$s") + textChapter?.let { + if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) { + nextParagraph() + } + if (pageIndex + 1 < it.pageSize + && readAloudNumber + 1 > it.getReadLength(pageIndex + 1) + ) { + pageIndex++ + ReadBook.moveToNextPage() + } + upTtsProgress(readAloudNumber + 1) + } + } + + override fun onDone(s: String) { + LogUtils.d(TAG, "onDone utteranceId:$s") + nextParagraph() + } + + override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) { + super.onRangeStart(utteranceId, start, end, frame) + val msg = + "onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame" + LogUtils.d(TAG, msg) + textChapter?.let { + if (pageIndex + 1 < it.pageSize + && readAloudNumber + start > it.getReadLength(pageIndex + 1) + ) { + pageIndex++ + ReadBook.moveToNextPage() + upTtsProgress(readAloudNumber + start) + } + } + } + + override fun onError(utteranceId: String?, errorCode: Int) { + LogUtils.d( + TAG, + "onError nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId errorCode:$errorCode" + ) + nextParagraph() + } + + private fun nextParagraph() { + //跳过全标点段落 + do { + readAloudNumber += contentList[nowSpeak].length + 1 - paragraphStartPos + paragraphStartPos = 0 + nowSpeak++ + if (nowSpeak >= contentList.size) { + nextChapter() + return + } + } while (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) + } + + @Deprecated("Deprecated in Java") + override fun onError(s: String) { + LogUtils.d(TAG, "onError nowSpeak:$nowSpeak pageIndex:$pageIndex s:$s") + nextParagraph() + } + + } + + override fun aloudServicePendingIntent(actionStr: String): PendingIntent? { + return servicePendingIntent(actionStr) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/WebService.kt b/app/src/main/java/io/legado/app/service/WebService.kt new file mode 100755 index 000000000..4d8b0fbf7 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/WebService.kt @@ -0,0 +1,230 @@ +package io.legado.app.service + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.wifi.WifiManager +import android.os.Build +import android.os.PowerManager +import androidx.core.app.NotificationCompat +import io.legado.app.R +import io.legado.app.base.BaseService +import io.legado.app.constant.AppConst +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.NotificationId +import io.legado.app.constant.PreferKey +import io.legado.app.receiver.NetworkChangedListener +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.postEvent +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.sendToClip +import io.legado.app.utils.servicePendingIntent +import io.legado.app.utils.startForegroundServiceCompat +import io.legado.app.utils.startService +import io.legado.app.utils.stopService +import io.legado.app.utils.toastOnUi +import io.legado.app.web.HttpServer +import io.legado.app.web.WebSocketServer +import splitties.init.appCtx +import splitties.systemservices.powerManager +import splitties.systemservices.wifiManager +import java.io.IOException + +class WebService : BaseService() { + + companion object { + var isRun = false + var hostAddress = "" + + fun start(context: Context) { + context.startService() + } + + fun startForeground(context: Context) { + val intent = Intent(context, WebService::class.java) + context.startForegroundServiceCompat(intent) + } + + fun stop(context: Context) { + context.stopService() + } + + fun serve() { + appCtx.startService { + action = "serve" + } + } + } + + private val useWakeLock = appCtx.getPrefBoolean(PreferKey.webServiceWakeLock, false) + private val wakeLock: PowerManager.WakeLock by lazy { + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:WebService") + .apply { + setReferenceCounted(false) + } + } + private val wifiLock by lazy { + @Suppress("DEPRECATION") + wifiManager?.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "legado:WebService") + ?.apply { + setReferenceCounted(false) + } + } + private var httpServer: HttpServer? = null + private var webSocketServer: WebSocketServer? = null + private var notificationList = mutableListOf(appCtx.getString(R.string.service_starting)) + private val networkChangedListener by lazy { + NetworkChangedListener(this) + } + + @SuppressLint("WakelockTimeout") + override fun onCreate() { + super.onCreate() + if (useWakeLock) { + wakeLock.acquire() + wifiLock?.acquire() + } + isRun = true + upTile(true) + networkChangedListener.register() + networkChangedListener.onNetworkChanged = { + val addressList = NetworkUtils.getLocalIPAddress() + notificationList.clear() + if (addressList.any()) { + notificationList.addAll(addressList.map { address -> + getString( + R.string.http_ip, + address.hostAddress, + getPort() + ) + }) + hostAddress = notificationList.first() + } else { + hostAddress = getString(R.string.network_connection_unavailable) + notificationList.add(hostAddress) + } + startForegroundNotification() + postEvent(EventBus.WEB_SERVICE, hostAddress) + } + } + + @SuppressLint("WakelockTimeout") + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.stop -> stopSelf() + "copyHostAddress" -> sendToClip(hostAddress) + "serve" -> if (useWakeLock) { + wakeLock.acquire() + wifiLock?.acquire() + } + + else -> upWebServer() + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + super.onDestroy() + if (useWakeLock) { + wakeLock.release() + wifiLock?.release() + } + networkChangedListener.unRegister() + isRun = false + if (httpServer?.isAlive == true) { + httpServer?.stop() + } + if (webSocketServer?.isAlive == true) { + webSocketServer?.stop() + } + postEvent(EventBus.WEB_SERVICE, "") + upTile(false) + } + + private fun upWebServer() { + if (httpServer?.isAlive == true) { + httpServer?.stop() + } + if (webSocketServer?.isAlive == true) { + webSocketServer?.stop() + } + val addressList = NetworkUtils.getLocalIPAddress() + if (addressList.any()) { + val port = getPort() + httpServer = HttpServer(port) + webSocketServer = WebSocketServer(port + 1) + try { + httpServer?.start() + webSocketServer?.start(1000 * 30) // 通信超时设置 + notificationList.clear() + notificationList.addAll(addressList.map { address -> + getString( + R.string.http_ip, + address.hostAddress, + getPort() + ) + }) + hostAddress = notificationList.first() + isRun = true + postEvent(EventBus.WEB_SERVICE, hostAddress) + startForegroundNotification() + } catch (e: IOException) { + toastOnUi(e.localizedMessage ?: "") + e.printOnDebug() + stopSelf() + } + } else { + toastOnUi("web service cant start, no ip address") + stopSelf() + } + } + + private fun getPort(): Int { + var port = getPrefInt(PreferKey.webPort, 1122) + if (port > 65530 || port < 1024) { + port = 1122 + } + return port + } + + /** + * 更新通知 + */ + override fun startForegroundNotification() { + val builder = NotificationCompat.Builder(this, AppConst.channelIdWeb) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setSmallIcon(R.drawable.ic_web_service_noti) + .setOngoing(true) + .setContentTitle(getString(R.string.web_service)) + .setContentText(notificationList.joinToString("\n")) + .setContentIntent( + servicePendingIntent("copyHostAddress") + ) + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.cancel), + servicePendingIntent(IntentAction.stop) + ) + val notification = builder.build() + startForeground(NotificationId.WebService, notification) + } + + @SuppressLint("ObsoleteSdkInt") + private fun upTile(active: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + kotlin.runCatching { + startService { + action = if (active) { + IntentAction.start + } else { + IntentAction.stop + } + } + } + + } + } +} diff --git a/app/src/main/java/io/legado/app/service/WebTileService.kt b/app/src/main/java/io/legado/app/service/WebTileService.kt new file mode 100755 index 000000000..68bd72c5d --- /dev/null +++ b/app/src/main/java/io/legado/app/service/WebTileService.kt @@ -0,0 +1,79 @@ +package io.legado.app.service + +import android.app.Dialog +import android.app.ForegroundServiceStartNotAllowedException +import android.content.Intent +import android.os.Build +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import android.view.WindowManager.BadTokenException +import androidx.annotation.RequiresApi +import io.legado.app.R +import io.legado.app.constant.IntentAction +import io.legado.app.utils.printOnDebug + + +/** + * web服务快捷开关 + */ +@RequiresApi(Build.VERSION_CODES.N) +class WebTileService : TileService() { + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + try { + when (intent?.action) { + IntentAction.start -> qsTile?.run { + state = Tile.STATE_ACTIVE + updateTile() + } + + IntentAction.stop -> qsTile?.run { + state = Tile.STATE_INACTIVE + updateTile() + } + } + } catch (e: Exception) { + e.printOnDebug() + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onStartListening() { + super.onStartListening() + qsTile?.run { + state = if (WebService.isRun) { + Tile.STATE_ACTIVE + } else { + Tile.STATE_INACTIVE + } + updateTile() + } + } + + override fun onClick() { + super.onClick() + if (WebService.isRun) { + WebService.stop(this) + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val dialog = Dialog(this, R.style.AppTheme_Transparent) + dialog.setOnShowListener { + try { + WebService.startForeground(this) + } catch (e: ForegroundServiceStartNotAllowedException) { + e.printStackTrace() + } + dialog.dismiss() + } + try { + showDialog(dialog) + } catch (e: BadTokenException) { + e.printStackTrace() + } + } else { + WebService.start(this) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/README.md b/app/src/main/java/io/legado/app/ui/README.md new file mode 100755 index 000000000..7c5ab259a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/README.md @@ -0,0 +1,26 @@ +# 放置与界面有关的类 + +* about 关于界面 +* association 导入书源界面 +* book\audio 音频播放界面 +* book\arrange 书架整理界面 +* book\info 书籍信息查看 +* book\read 书籍阅读界面 +* book\search 搜索书籍界面 +* book\source 书源界面 +* book\changeCover 封面换源界面 +* book\changeSource 换源界面 +* book\toc 目录界面 +* book\download 下载界面 +* book\explore 发现界面 +* book\local 书籍导入界面 +* document 文件选择界面 +* config 配置界面 +* main 主界面 +* qrCode 二维码扫描界面 +* replaceRule 替换净化界面 +* rss\article 订阅条目界面 +* rss\read 订阅阅读界面 +* rss\source 订阅源界面 +* welcome 欢迎界面 +* widget 自定义插件 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt new file mode 100755 index 000000000..abc30b731 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt @@ -0,0 +1,62 @@ +package io.legado.app.ui.about + +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.view.Menu +import android.view.MenuItem +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.databinding.ActivityAboutBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.filletBackground +import io.legado.app.utils.openUrl +import io.legado.app.utils.share +import io.legado.app.utils.viewbindingdelegate.viewBinding + + +class AboutActivity : BaseActivity() { + + override val binding by viewBinding(ActivityAboutBinding::inflate) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.llAbout.background = filletBackground + val fTag = "aboutFragment" + var aboutFragment = supportFragmentManager.findFragmentByTag(fTag) + if (aboutFragment == null) aboutFragment = AboutFragment() + supportFragmentManager.beginTransaction() + .replace(R.id.fl_fragment, aboutFragment, fTag) + .commit() + binding.tvAppSummary.post { + kotlin.runCatching { + val span = ForegroundColorSpan(accentColor) + val spannableString = SpannableString(binding.tvAppSummary.text) + val gzh = getString(R.string.legado_gzh) + val start = spannableString.indexOf(gzh) + spannableString.setSpan( + span, start, start + gzh.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + binding.tvAppSummary.text = spannableString + } + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.about, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_scoring -> openUrl("market://details?id=$packageName") + R.id.menu_share_it -> share( + getString(R.string.app_share_description), + getString(R.string.app_name) + ) + } + return super.onCompatOptionsItemSelected(item) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt new file mode 100755 index 000000000..6ee532c6a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt @@ -0,0 +1,214 @@ +package io.legado.app.ui.about + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.annotation.StringRes +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import io.legado.app.R +import io.legado.app.constant.AppConst.appInfo +import io.legado.app.constant.AppLog +import io.legado.app.help.CrashHandler +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.update.AppUpdate +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.FileDoc +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.createFolderIfNotExist +import io.legado.app.utils.delete +import io.legado.app.utils.externalCache +import io.legado.app.utils.find +import io.legado.app.utils.list +import io.legado.app.utils.openInputStream +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.openUrl +import io.legado.app.utils.sendMail +import io.legado.app.utils.sendToClip +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.delay +import splitties.init.appCtx +import java.io.File + +class AboutFragment : PreferenceFragmentCompat() { + + private val waitDialog by lazy { + WaitDialog(requireContext()) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.about) + findPreference("update_log")?.summary = + "${getString(R.string.version)} ${appInfo.versionName}" + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listView.overScrollMode = View.OVER_SCROLL_NEVER + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + "contributors" -> openUrl(R.string.contributors_url) + "update_log" -> showMdFile(getString(R.string.update_log), "updateLog.md") + "check_update" -> checkUpdate() + "mail" -> requireContext().sendMail(getString(R.string.email)) + "license" -> showMdFile(getString(R.string.license), "LICENSE.md") + "disclaimer" -> showMdFile(getString(R.string.disclaimer), "disclaimer.md") + "privacyPolicy" -> showMdFile(getString(R.string.privacy_policy), "privacyPolicy.md") + "gzGzh" -> requireContext().sendToClip(getString(R.string.legado_gzh)) + "crashLog" -> showDialogFragment() + "saveLog" -> saveLog() + "createHeapDump" -> createHeapDump() + } + return super.onPreferenceTreeClick(preference) + } + + @Suppress("SameParameterValue") + private fun openUrl(@StringRes addressID: Int) { + requireContext().openUrl(getString(addressID)) + } + + /** + * 显示md文件 + */ + private fun showMdFile(title: String, fileName: String) { + val mdText = String(requireContext().assets.open(fileName).readBytes()) + showDialogFragment(TextDialog(title, mdText, TextDialog.Mode.MD)) + } + + /** + * 检测更新 + */ + private fun checkUpdate() { + waitDialog.show() + AppUpdate.gitHubUpdate?.run { + check(lifecycleScope) + .onSuccess { + showDialogFragment( + UpdateDialog(it) + ) + }.onError { + appCtx.toastOnUi("${getString(R.string.check_update)}\n${it.localizedMessage}") + }.onFinally { + waitDialog.dismiss() + } + } + } + + + /** + * 加入qq群 + */ + private fun joinQQGroup(key: String): Boolean { + val intent = Intent() + intent.data = + Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D$key") + // 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面 + // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + kotlin.runCatching { + startActivity(intent) + return true + }.onFailure { + toastOnUi("添加失败,请手动添加") + } + return false + } + + private fun saveLog() { + Coroutine.async { + val backupPath = AppConfig.backupPath ?: let { + appCtx.toastOnUi("未设置备份目录") + return@async + } + if (!AppConfig.recordLog) { + appCtx.toastOnUi("未开启日志记录,请去其他设置里打开记录日志") + delay(3000) + } + val doc = FileDoc.fromUri(Uri.parse(backupPath), true) + copyLogs(doc) + copyHeapDump(doc) + appCtx.toastOnUi("已保存至备份目录") + }.onError { + AppLog.put("保存日志出错\n${it.localizedMessage}", it, true) + } + } + + private fun createHeapDump() { + Coroutine.async { + val backupPath = AppConfig.backupPath ?: let { + appCtx.toastOnUi("未设置备份目录") + return@async + } + if (!AppConfig.recordHeapDump) { + appCtx.toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储") + delay(3000) + } + appCtx.toastOnUi("开始创建堆转储") + System.gc() + CrashHandler.doHeapDump(true) + val doc = FileDoc.fromUri(Uri.parse(backupPath), true) + if (!copyHeapDump(doc)) { + appCtx.toastOnUi("未找到堆转储文件") + } else { + appCtx.toastOnUi("已保存至备份目录") + } + }.onError { + AppLog.put("保存堆转储失败\n${it.localizedMessage}", it) + } + } + + private fun copyLogs(doc: FileDoc) { + val cacheDir = appCtx.externalCache + val logFiles = File(cacheDir, "logs") + val crashFiles = File(cacheDir, "crash") + val logcatFile = File(cacheDir, "logcat.txt") + + dumpLogcat(logcatFile) + + val zipFile = File(cacheDir, "logs.zip") + ZipUtils.zipFiles(arrayListOf(logFiles, crashFiles, logcatFile), zipFile) + + doc.find("logs.zip")?.delete() + + zipFile.inputStream().use { input -> + doc.createFileIfNotExist("logs.zip").openOutputStream().getOrNull() + ?.use { + input.copyTo(it) + } + } + zipFile.delete() + } + + private fun copyHeapDump(doc: FileDoc): Boolean { + val heapFile = FileDoc.fromFile(File(appCtx.externalCache, "heapDump")).list() + ?.firstOrNull() ?: return false + doc.find("heapDump")?.delete() + val heapDumpDoc = doc.createFolderIfNotExist("heapDump") + heapFile.openInputStream().getOrNull()?.use { input -> + heapDumpDoc.createFileIfNotExist(heapFile.name).openOutputStream().getOrNull() + ?.use { + input.copyTo(it) + } + } + return true + } + + private fun dumpLogcat(file: File) { + try { + val process = Runtime.getRuntime().exec("logcat -d") + file.outputStream().use { + process.inputStream.copyTo(it) + } + } catch (e: Exception) { + AppLog.put("保存Logcat失败\n$e", e) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/AppLogDialog.kt b/app/src/main/java/io/legado/app/ui/about/AppLogDialog.kt new file mode 100755 index 000000000..ef5479315 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/AppLogDialog.kt @@ -0,0 +1,90 @@ +package io.legado.app.ui.about + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemAppLogBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.LogUtils +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.views.onClick +import java.util.* + +class AppLogDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { + LogAdapter(requireContext()) + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.setTitle(R.string.log) + toolBar.inflateMenu(R.menu.app_log) + toolBar.setOnMenuItemClickListener(this@AppLogDialog) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter + } + adapter.setItems(AppLog.logs) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_clear -> { + AppLog.clear() + adapter.clearItems() + } + } + return true + } + + inner class LogAdapter(context: Context) : + RecyclerAdapter, ItemAppLogBinding>(context) { + + override fun getViewBinding(parent: ViewGroup): ItemAppLogBinding { + return ItemAppLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemAppLogBinding, + item: Triple, + payloads: MutableList + ) { + binding.textTime.text = LogUtils.logTimeFormat.format(Date(item.first)) + binding.textMessage.text = item.second + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemAppLogBinding) { + binding.root.onClick { + getItem(holder.layoutPosition)?.let { item -> + item.third?.let { + showDialogFragment(TextDialog("Log", it.stackTraceToString())) + } + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/CrashLogsDialog.kt b/app/src/main/java/io/legado/app/ui/about/CrashLogsDialog.kt new file mode 100755 index 000000000..64c651630 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/CrashLogsDialog.kt @@ -0,0 +1,166 @@ +package io.legado.app.ui.about + +import android.app.Application +import android.net.Uri +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.BaseViewModel +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.Item1lineTextBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.delete +import io.legado.app.utils.find +import io.legado.app.utils.getFile +import io.legado.app.utils.list +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.isActive +import java.io.FileFilter + +class CrashLogsDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { LogAdapter() } + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.crash_log) + binding.toolBar.inflateMenu(R.menu.crash_log) + binding.toolBar.setOnMenuItemClickListener(this) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + viewModel.logLiveData.observe(viewLifecycleOwner) { + adapter.setItems(it) + } + viewModel.initData() + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_clear -> viewModel.clearCrashLog() + } + return true + } + + private fun showLogFile(fileDoc: FileDoc) { + viewModel.readFile(fileDoc) { + if (lifecycleScope.isActive) { + showDialogFragment(TextDialog(fileDoc.name, it)) + } + } + + } + + inner class LogAdapter : RecyclerAdapter(requireContext()) { + + override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding { + return Item1lineTextBinding.inflate(inflater, parent, false) + } + + override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) { + binding.root.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { item -> + showLogFile(item) + } + } + } + + override fun convert( + holder: ItemViewHolder, + binding: Item1lineTextBinding, + item: FileDoc, + payloads: MutableList + ) { + binding.textView.text = item.name + } + + } + + class CrashViewModel(application: Application) : BaseViewModel(application) { + + val logLiveData = MutableLiveData>() + + fun initData() { + execute { + val list = arrayListOf() + context.externalCacheDir + ?.getFile("crash") + ?.listFiles(FileFilter { it.isFile }) + ?.forEach { + list.add(FileDoc.fromFile(it)) + } + val backupPath = AppConfig.backupPath + if (!backupPath.isNullOrEmpty()) { + val uri = Uri.parse(backupPath) + FileDoc.fromUri(uri, true) + .find("crash") + ?.list { + !it.isDir + }?.let { + list.addAll(it) + } + } + return@execute list.sortedByDescending { it.name }.distinctBy { it.name } + }.onSuccess { + logLiveData.postValue(it) + } + } + + fun readFile(fileDoc: FileDoc, success: (String) -> Unit) { + execute { + String(fileDoc.readBytes()) + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + + fun clearCrashLog() { + execute { + context.externalCacheDir + ?.getFile("crash") + ?.let { + FileUtils.delete(it, false) + } + val backupPath = AppConfig.backupPath + if (!backupPath.isNullOrEmpty()) { + val uri = Uri.parse(backupPath) + FileDoc.fromUri(uri, true) + .find("crash") + ?.delete() + } + }.onError { + context.toastOnUi(it.localizedMessage) + }.onFinally { + initData() + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt b/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt new file mode 100755 index 000000000..6b6c32414 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt @@ -0,0 +1,235 @@ +package io.legado.app.ui.about + +import android.content.Context +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.ViewGroup +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReadRecordShow +import io.legado.app.databinding.ActivityReadRecordBinding +import io.legado.app.databinding.ItemReadRecordBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.applyTint +import io.legado.app.utils.cnCompare +import io.legado.app.utils.getInt +import io.legado.app.utils.putInt +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.SimpleDateFormat +import java.util.Locale + +class ReadRecordActivity : BaseActivity() { + + private val adapter by lazy { RecordAdapter(this) } + private var sortMode + get() = LocalConfig.getInt("readRecordSort") + set(value) { + LocalConfig.putInt("readRecordSort", value) + } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + + override val binding by viewBinding(ActivityReadRecordBinding::inflate) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initAllTime() + initData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_read_record, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_enable_record)?.isChecked = AppConfig.enableReadRecord + when (sortMode) { + 1 -> menu.findItem(R.id.menu_sort_read_long)?.isChecked = true + 2 -> menu.findItem(R.id.menu_sort_read_time)?.isChecked = true + else -> menu.findItem(R.id.menu_sort_name)?.isChecked = true + } + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_sort_name -> { + sortMode = 0 + item.isChecked = true + initData() + } + + R.id.menu_sort_read_long -> { + sortMode = 1 + item.isChecked = true + initData() + } + + R.id.menu_sort_read_time -> { + sortMode = 2 + item.isChecked = true + initData() + } + + R.id.menu_enable_record -> { + AppConfig.enableReadRecord = !item.isChecked + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + initSearchView() + binding.tvBookName.setText(R.string.all_read_time) + binding.tvRemove.setOnClickListener { + alert(R.string.delete, R.string.sure_del) { + yesButton { + appDb.readRecordDao.clear() + initData() + } + noButton() + } + } + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + searchView.clearFocus() + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + initData(newText) + return false + } + }) + } + + private fun initAllTime() { + lifecycleScope.launch { + val allTime = withContext(IO) { + appDb.readRecordDao.allTime + } + binding.tvReadingTime.text = formatDuring(allTime) + } + } + + private fun initData(searchKey: String? = null) { + lifecycleScope.launch { + val readRecords = withContext(IO) { + appDb.readRecordDao.search(searchKey ?: "").let { records -> + when (sortMode) { + 1 -> records.sortedByDescending { it.readTime } + 2 -> records.sortedByDescending { it.lastRead } + else -> records.sortedWith { o1, o2 -> + o1.bookName.cnCompare(o2.bookName) + } + } + } + } + adapter.setItems(readRecords) + } + } + + inner class RecordAdapter(context: Context) : + RecyclerAdapter(context) { + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + override fun getViewBinding(parent: ViewGroup): ItemReadRecordBinding { + return ItemReadRecordBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemReadRecordBinding, + item: ReadRecordShow, + payloads: MutableList, + ) { + binding.apply { + tvBookName.text = item.bookName + tvReadingTime.text = formatDuring(item.readTime) + if (item.lastRead > 0) { + tvLastReadTime.text = dateFormat.format(item.lastRead) + } else { + tvLastReadTime.text = "" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemReadRecordBinding) { + binding.apply { + root.setOnClickListener { + val item = getItem(holder.layoutPosition) ?: return@setOnClickListener + lifecycleScope.launch { + val book = withContext(IO) { + appDb.bookDao.findByName(item.bookName).firstOrNull() + } + if (book == null) { + SearchActivity.start(this@ReadRecordActivity, item.bookName) + } else { + startActivityForBook(book) + } + } + } + tvRemove.setOnClickListener { + getItem(holder.layoutPosition)?.let { item -> + sureDelAlert(item) + } + } + } + } + + private fun sureDelAlert(item: ReadRecordShow) { + alert(R.string.delete) { + setMessage(getString(R.string.sure_del_any, item.bookName)) + yesButton { + appDb.readRecordDao.deleteByName(item.bookName) + initData() + } + noButton() + } + } + + } + + fun formatDuring(mss: Long): String { + val days = mss / (1000 * 60 * 60 * 24) + val hours = mss % (1000 * 60 * 60 * 24) / (1000 * 60 * 60) + val minutes = mss % (1000 * 60 * 60) / (1000 * 60) + val seconds = mss % (1000 * 60) / 1000 + val d = if (days > 0) "${days}天" else "" + val h = if (hours > 0) "${hours}小时" else "" + val m = if (minutes > 0) "${minutes}分钟" else "" + val s = if (seconds > 0) "${seconds}秒" else "" + var time = "$d$h$m$s" + if (time.isBlank()) { + time = "0秒" + } + return time + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/UpdateDialog.kt b/app/src/main/java/io/legado/app/ui/about/UpdateDialog.kt new file mode 100755 index 000000000..fcbb15224 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/UpdateDialog.kt @@ -0,0 +1,71 @@ +package io.legado.app.ui.about + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogUpdateBinding +import io.legado.app.help.update.AppUpdate +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.Download +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.noties.markwon.Markwon +import io.noties.markwon.ext.tables.TablePlugin +import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.image.glide.GlideImagesPlugin + +class UpdateDialog() : BaseDialogFragment(R.layout.dialog_update) { + + constructor(updateInfo: AppUpdate.UpdateInfo) : this() { + arguments = Bundle().apply { + putString("newVersion", updateInfo.tagName) + putString("updateBody", updateInfo.updateLog) + putString("url", updateInfo.downloadUrl) + putString("name", updateInfo.fileName) + } + } + + val binding by viewBinding(DialogUpdateBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = arguments?.getString("newVersion") + val updateBody = arguments?.getString("updateBody") + if (updateBody == null) { + toastOnUi("没有数据") + dismiss() + return + } + binding.textView.post { + Markwon.builder(requireContext()) + .usePlugin(GlideImagesPlugin.create(requireContext())) + .usePlugin(HtmlPlugin.create()) + .usePlugin(TablePlugin.create(requireContext())) + .build() + .setMarkdown(binding.textView, updateBody) + } + binding.toolBar.inflateMenu(R.menu.app_update) + binding.toolBar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_download -> { + val url = arguments?.getString("url") + val name = arguments?.getString("name") + if (url != null && name != null) { + Download.start(requireContext(), url, name) + toastOnUi(R.string.download_start) + } + } + } + return@setOnMenuItemClickListener true + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt b/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt new file mode 100755 index 000000000..b038f96e1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt @@ -0,0 +1,177 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.app.Application +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.DialogAddToBookshelfBinding +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.webBook.WebBook +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.utils.GSON +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.setLayout +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 添加书籍链接到书架,需要对应网站书源 + * ${origin}/${path}, {origin: bookSourceUrl} + * 按以下顺序尝试匹配书源并添加网址 + * - UrlOption中的指定的书源网址bookSourceUrl + * - 在所有启用的书源中匹配orgin + * - 在所有启用的书源中使用详情页正则匹配${origin}/${path}, {origin: bookSourceUrl} + */ +class AddToBookshelfDialog() : BaseDialogFragment(R.layout.dialog_add_to_bookshelf) { + + constructor(bookUrl: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("bookUrl", bookUrl) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + val binding by viewBinding(DialogAddToBookshelfBinding::bind) + val viewModel by viewModels() + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("SetTextI18n") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val bookUrl = arguments?.getString("bookUrl") + if (bookUrl.isNullOrBlank()) { + toastOnUi("url不能为空") + dismiss() + return + } + viewModel.loadStateLiveData.observe(this) { + if (it) { + binding.rotateLoading.visible() + } else { + binding.rotateLoading.gone() + } + } + viewModel.loadErrorLiveData.observe(this) { + toastOnUi(it) + dismiss() + } + viewModel.load(bookUrl) { + viewModel.saveSearchBook(it) { + startActivity { + putExtra("name", it.name) + putExtra("author", it.author) + putExtra("bookUrl", it.bookUrl) + } + dismiss() + } + } + binding.tvCancel.setOnClickListener { + dismiss() + } + } + + class ViewModel(application: Application) : BaseViewModel(application) { + + val loadStateLiveData = MutableLiveData() + val loadErrorLiveData = MutableLiveData() + var book: Book? = null + + fun load(bookUrl: String, success: (book: Book) -> Unit) { + execute { + appDb.bookDao.getBook(bookUrl)?.let { + throw NoStackTraceException("${it.name} 已在书架") + } + val baseUrl = NetworkUtils.getBaseUrl(bookUrl) + ?: throw NoStackTraceException("书籍地址格式不对") + val urlMatcher = AnalyzeUrl.paramPattern.matcher(bookUrl) + if (urlMatcher.find()) { + val origin = GSON.fromJsonObject( + bookUrl.substring(urlMatcher.end()) + ).getOrNull()?.getOrigin() + origin?.let { + val source = appDb.bookSourceDao.getBookSource(it) + source?.let { + getBookInfo(bookUrl, source)?.let { book -> + return@execute book + } + } + } + } + appDb.bookSourceDao.getBookSourceAddBook(baseUrl)?.let { source -> + getBookInfo(bookUrl, source)?.let { book -> + return@execute book + } + } + appDb.bookSourceDao.hasBookUrlPattern.forEach { source -> + try { + val bs = source.getBookSource()!! + if (bookUrl.matches(bs.bookUrlPattern!!.toRegex())) { + getBookInfo(bookUrl, bs)?.let { book -> + return@execute book + } + } + } catch (_: Exception) { + } + } + throw NoStackTraceException("未找到匹配书源") + }.onError { + AppLog.put("添加书籍 $bookUrl 出错", it) + loadErrorLiveData.postValue(it.localizedMessage) + }.onSuccess { + book = it + success.invoke(it) + }.onStart { + loadStateLiveData.postValue(true) + }.onFinally { + loadStateLiveData.postValue(false) + } + } + + private suspend fun getBookInfo(bookUrl: String, source: BookSource): Book? { + return kotlin.runCatching { + val book = Book( + bookUrl = bookUrl, + origin = source.bookSourceUrl, + originName = source.bookSourceName + ) + WebBook.getBookInfoAwait(source, book) + }.getOrNull() + } + + fun saveSearchBook(book: Book, success: () -> Unit) { + execute { + val searchBook = book.toSearchBook() + appDb.searchBookDao.insert(searchBook) + searchBook + }.onSuccess { + success.invoke() + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt new file mode 100755 index 000000000..81a61ba07 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/BaseAssociationViewModel.kt @@ -0,0 +1,48 @@ +package io.legado.app.ui.association + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.utils.inputStream +import io.legado.app.utils.jsonPath + +abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) { + + val successLive = MutableLiveData>() + val errorLive = MutableLiveData() + + fun importJson(uri: Uri) { + val map = uri.inputStream(context).getOrThrow().use { + jsonPath.parse(it).read>("$[0]") + } ?: uri.inputStream(context).getOrThrow().use { + jsonPath.parse(it).read("$") + } + + when { + map.containsKey("bookSourceUrl") -> + successLive.postValue("bookSource" to uri.toString()) + + map.containsKey("sourceUrl") -> + successLive.postValue("rssSource" to uri.toString()) + + map.containsKey("pattern") -> + successLive.postValue("replaceRule" to uri.toString()) + + map.containsKey("themeName") -> + successLive.postValue("theme" to uri.toString()) + + map.containsKey("showRule") -> + successLive.postValue("dictRule" to uri.toString()) + + map.containsKey("name") && map.containsKey("rule") -> + successLive.postValue("txtRule" to uri.toString()) + + map.containsKey("name") && map.containsKey("url") -> + successLive.postValue("httpTts" to uri.toString()) + + else -> errorLive.postValue("格式不对") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt new file mode 100755 index 000000000..268bcc387 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt @@ -0,0 +1,219 @@ +package io.legado.app.ui.association + +import android.net.Uri +import android.os.Bundle +import androidx.activity.viewModels +import androidx.core.os.postDelayed +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.exception.InvalidBooksDirException +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.FileUtils +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.canRead +import io.legado.app.utils.checkWrite +import io.legado.app.utils.getFile +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.readUri +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.io.File +import java.io.FileOutputStream + +class FileAssociationActivity : + VMBaseActivity() { + + private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { + intent.data?.let { uri -> + it.uri?.let { treeUri -> + AppConfig.defaultBookTreeUri = treeUri.toString() + importBook(treeUri, uri) + } ?: let { + val storageHelp = String(assets.open("storageHelp.md").readBytes()) + toastOnUi(storageHelp) + importBook(null, uri) + } + } + } + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + + override val viewModel by viewModels() + + private val handler by lazy { + buildMainHandler() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.rotateLoading.visible() + viewModel.importBookLiveData.observe(this) { uri -> + importBook(uri) + } + viewModel.onLineImportLive.observe(this) { + startActivity { + data = it + } + finish() + } + viewModel.successLive.observe(this) { + when (it.first) { + "bookSource" -> showDialogFragment(ImportBookSourceDialog(it.second, true)) + "rssSource" -> showDialogFragment(ImportRssSourceDialog(it.second, true)) + "replaceRule" -> showDialogFragment(ImportReplaceRuleDialog(it.second, true)) + "httpTts" -> showDialogFragment(ImportHttpTtsDialog(it.second, true)) + "theme" -> showDialogFragment(ImportThemeDialog(it.second, true)) + "txtRule" -> showDialogFragment(ImportTxtTocRuleDialog(it.second, true)) + "dictRule" -> showDialogFragment(ImportDictRuleDialog(it.second, true)) + } + } + viewModel.errorLive.observe(this) { + binding.rotateLoading.gone() + toastOnUi(it) + handler.postDelayed(2000) { + finish() + } + } + viewModel.openBookLiveData.observe(this) { + binding.rotateLoading.gone() + startActivityForBook(it) + finish() + } + viewModel.notSupportedLiveData.observe(this) { data -> + binding.rotateLoading.gone() + alert( + title = appCtx.getString(R.string.draw), + message = appCtx.getString(R.string.file_not_supported, data.second) + ) { + yesButton { + importBook(data.first) + } + noButton { + finish() + } + onCancelled { + finish() + } + } + } + intent.data?.let { data -> + if (data.isContentScheme() && data.canRead()) { + viewModel.dispatchIntent(data) + } else { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + viewModel.dispatchIntent(data) + }.onDenied { + toastOnUi("请求存储权限失败。") + handler.postDelayed(2000) { + finish() + } + }.request() + } + } ?: finish() + } + + private fun importBook(uri: Uri) { + if (uri.isContentScheme()) { + val treeUriStr = AppConfig.defaultBookTreeUri + if (treeUriStr.isNullOrEmpty()) { + localBookTreeSelect.launch { + title = getString(R.string.select_book_folder) + mode = HandleFileContract.DIR_SYS + } + } else { + importBook(Uri.parse(treeUriStr), uri) + } + } else { + importBook(null, uri) + } + } + + private fun importBook(treeUri: Uri?, uri: Uri) { + lifecycleScope.launch { + runCatching { + withContext(IO) { + if (treeUri == null) { + viewModel.importBook(uri) + } else if (treeUri.isContentScheme()) { + val treeDoc = + DocumentFile.fromTreeUri(this@FileAssociationActivity, treeUri) + if (!treeDoc!!.checkWrite()) { + throw InvalidBooksDirException( + "请重新设置书籍保存位置\nPermission Denial" + ) + } + readUri(uri) { fileDoc, inputStream -> + val name = fileDoc.name + var doc = treeDoc.findFile(name) + if (doc == null || fileDoc.lastModified > doc.lastModified()) { + if (doc == null) { + doc = treeDoc.createFile(FileUtils.getMimeType(name), name) + ?: throw InvalidBooksDirException( + "请重新设置书籍保存位置\nPermission Denial" + ) + } + contentResolver.openOutputStream(doc.uri)!!.use { oStream -> + inputStream.copyTo(oStream) + oStream.flush() + } + } + viewModel.importBook(doc.uri) + } + } else { + val treeFile = File(treeUri.path ?: treeUri.toString()) + if (!treeFile.checkWrite()) { + throw InvalidBooksDirException( + "请重新设置书籍保存位置\nPermission Denial" + ) + } + readUri(uri) { fileDoc, inputStream -> + val name = fileDoc.name + val file = treeFile.getFile(name) + if (!file.exists() || fileDoc.lastModified > file.lastModified()) { + FileOutputStream(file).use { oStream -> + inputStream.copyTo(oStream) + oStream.flush() + } + } + viewModel.importBook(Uri.fromFile(file)) + } + } + } + }.onFailure { + when (it) { + is InvalidBooksDirException -> localBookTreeSelect.launch { + title = getString(R.string.select_book_folder) + mode = HandleFileContract.DIR_SYS + } + + else -> { + val msg = "导入书籍失败\n${it.localizedMessage}" + AppLog.put(msg, it) + toastOnUi(msg) + handler.postDelayed(2000) { + finish() + } + } + } + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt new file mode 100755 index 000000000..08a03674c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt @@ -0,0 +1,66 @@ +package io.legado.app.ui.association + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.AppPattern.bookFileRegex +import io.legado.app.data.entities.Book +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.* + +class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) { + val importBookLiveData = MutableLiveData() + val onLineImportLive = MutableLiveData() + val openBookLiveData = MutableLiveData() + val notSupportedLiveData = MutableLiveData>() + + fun dispatchIntent(uri: Uri) { + execute { + //如果是普通的url,需要根据返回的内容判断是什么 + if (uri.isContentScheme() || uri.isFileScheme()) { + val fileDoc = FileDoc.fromUri(uri, false) + val fileName = fileDoc.name + if (fileName.matches(AppPattern.archiveFileRegex)) { + ArchiveUtils.deCompress(fileDoc, ArchiveUtils.TEMP_PATH) { + it.matches(bookFileRegex) + }.forEach { + dispatch(FileDoc.fromFile(it)) + } + } else { + dispatch(fileDoc) + } + } else { + onLineImportLive.postValue(uri) + } + }.onError { + it.printOnDebug() + val msg = "无法打开文件\n${it.localizedMessage}" + errorLive.postValue(msg) + AppLog.put(msg, it) + } + } + + private fun dispatch(fileDoc: FileDoc) { + kotlin.runCatching { + if (fileDoc.openInputStream().getOrNull().isJson()) { + importJson(fileDoc.uri) + return + } + }.onFailure { + it.printOnDebug() + AppLog.put("尝试导入为JSON文件失败\n${it.localizedMessage}", it) + } + if (fileDoc.name.matches(bookFileRegex)) { + importBookLiveData.postValue(fileDoc.uri) + return + } + notSupportedLiveData.postValue(Pair(fileDoc.uri, fileDoc.name)) + } + + fun importBook(uri: Uri) { + val book = LocalBook.importFile(uri) + openBookLiveData.postValue(book) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt new file mode 100755 index 000000000..a8d823921 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt @@ -0,0 +1,288 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.DialogCustomGroupBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.dpToPx +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + + +/** + * 导入书源弹出窗口 + */ +class ImportBookSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener, + CodeDialog.Callback { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_book_source) + binding.rotateLoading.visible() + initMenu() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.import_source) + binding.toolBar.menu.findItem(R.id.menu_keep_original_name) + ?.isChecked = AppConfig.importKeepName + binding.toolBar.menu.findItem(R.id.menu_keep_group) + ?.isChecked = AppConfig.importKeepGroup + binding.toolBar.menu.findItem(R.id.menu_keep_enable) + ?.isChecked = AppConfig.importKeepEnable + } + + @SuppressLint("InflateParams", "NotifyDataSetChanged") + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_new_group -> alertCustomGroup(item) + R.id.menu_select_new_source -> { + val selectAllNew = viewModel.isSelectAllNew + viewModel.newSourceStatus.forEachIndexed { index, b -> + if (b) { + viewModel.selectStatus[index] = !selectAllNew + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + + R.id.menu_select_update_source -> { + val selectAllUpdate = viewModel.isSelectAllUpdate + viewModel.updateSourceStatus.forEachIndexed { index, b -> + if (b) { + viewModel.selectStatus[index] = !selectAllUpdate + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + + R.id.menu_keep_original_name -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepName, item.isChecked) + } + + R.id.menu_keep_group -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepGroup, item.isChecked) + } + + R.id.menu_keep_enable -> { + item.isChecked = !item.isChecked + AppConfig.importKeepEnable = item.isChecked + } + } + return false + } + + private fun alertCustomGroup(item: MenuItem) { + alert(R.string.diy_edit_source_group) { + val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply { + val groups = appDb.bookSourceDao.allGroups() + textInputLayout.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { + alertBinding.root + } + okButton { + viewModel.isAddGroup = alertBinding.swAddGroup.isChecked + viewModel.groupName = alertBinding.editView.text?.toString() + if (viewModel.groupName.isNullOrBlank()) { + item.title = getString(R.string.diy_source_group) + } else { + val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName) + if (viewModel.isAddGroup) { + item.title = "+$group" + } else { + item.title = group + } + } + } + cancelButton() + } + } + + override fun onCodeSave(code: String, requestId: String?) { + requestId?.toInt()?.let { + GSON.fromJsonObject(code).getOrNull()?.let { source -> + viewModel.allSources[it] = source + adapter.setItem(it, source) + } + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: BookSource, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.bookSourceName + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when { + localSource == null -> "新增" + item.lastUpdateTime > localSource.lastUpdateTime -> "更新" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt new file mode 100755 index 000000000..350123e10 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt @@ -0,0 +1,221 @@ +package io.legado.app.ui.association + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import com.jayway.jsonpath.JsonPath +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.config.AppConfig +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.inputStream +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.splitNotBlank + + +class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { + var isAddGroup = false + var groupName: String? = null + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + val newSourceStatus = arrayListOf() + val updateSourceStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val isSelectAllNew: Boolean + get() { + newSourceStatus.forEachIndexed { index, b -> + if (b && !selectStatus[index]) { + return false + } + } + return true + } + + val isSelectAllUpdate: Boolean + get() { + updateSourceStatus.forEachIndexed { index, b -> + if (b && !selectStatus[index]) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val group = groupName?.trim() + val keepName = AppConfig.importKeepName + val keepGroup = AppConfig.importKeepGroup + val keepEnable = AppConfig.importKeepEnable + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + val source = allSources[index] + checkSources[index]?.let { + if (keepName) { + source.bookSourceName = it.bookSourceName + } + if (keepGroup) { + source.bookSourceGroup = it.bookSourceGroup + } + if (keepEnable) { + source.enabled = it.enabled + source.enabledExplore = it.enabledExplore + } + source.customOrder = it.customOrder + } + if (!group.isNullOrEmpty()) { + if (isAddGroup) { + val groups = linkedSetOf() + source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.let { + groups.addAll(it) + } + groups.add(group) + source.bookSourceGroup = groups.joinToString(",") + } else { + source.bookSourceGroup = group + } + } + selectSource.add(source) + } + } + SourceHelp.insertBookSource(*selectSource.toTypedArray()) + ContentProcessor.upReplaceRules() + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + val mText = text.trim() + when { + mText.isJsonObject() -> { + kotlin.runCatching { + val json = JsonPath.parse(mText) + json.read>("$.sourceUrls") + }.onSuccess { listUrl -> + listUrl.forEach { + importSourceUrl(it) + } + }.onFailure { + GSON.fromJsonObject(mText).getOrThrow().let { + if (it.bookSourceUrl.isEmpty()) { + throw NoStackTraceException("不是书源") + } + allSources.add(it) + } + } + } + + mText.isJsonArray() -> GSON.fromJsonArray(mText).getOrThrow() + .let { items -> + val source = items.firstOrNull() ?: return@let + if (source.bookSourceUrl.isEmpty()) { + throw NoStackTraceException("不是书源") + } + allSources.addAll(items) + } + + mText.isAbsUrl() -> { + importSourceUrl(mText) + } + + mText.isUri() -> { + val uri = Uri.parse(mText) + uri.inputStream(context).getOrThrow().use { inputS -> + GSON.fromJsonArray(inputS).getOrThrow().let { + val source = it.firstOrNull() ?: return@let + if (source.bookSourceUrl.isEmpty()) { + throw NoStackTraceException("不是书源") + } + allSources.addAll(it) + } + } + } + + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().byteStream().use { + GSON.fromJsonArray(it).getOrThrow().let { list -> + val source = list.firstOrNull() ?: return@let + if (source.bookSourceUrl.isEmpty()) { + throw NoStackTraceException("不是书源") + } + allSources.addAll(list) + } + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { + val source = appDb.bookSourceDao.getBookSourcePart(it.bookSourceUrl) + checkSources.add(source) + selectStatus.add(source == null || source.lastUpdateTime < it.lastUpdateTime) + newSourceStatus.add(source == null) + updateSourceStatus.add(source != null && source.lastUpdateTime < it.lastUpdateTime) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt new file mode 100755 index 000000000..5d3946ed9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleDialog.kt @@ -0,0 +1,187 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.DictRule +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + +class ImportDictRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + CodeDialog.Callback { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_dict_rule) + binding.rotateLoading.visible() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: DictRule, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.name + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when (localSource) { + null -> "新增" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } + + override fun onCodeSave(code: String, requestId: String?) { + requestId?.toInt()?.let { + GSON.fromJsonObject(code).getOrNull()?.let { source -> + viewModel.allSources[it] = source + adapter.setItem(it, source) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt new file mode 100755 index 000000000..45d68f1bc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportDictRuleViewModel.kt @@ -0,0 +1,130 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.readText +import splitties.init.appCtx + +class ImportDictRuleViewModel(app: Application) : BaseViewModel(app) { + + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + selectSource.add(allSources[index]) + } + } + appDb.dictRuleDao.insert(*selectSource.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + importSourceAwait(text.trim()) + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceAwait(text: String) { + when { + text.isJsonObject() -> { + GSON.fromJsonObject(text).getOrThrow().let { + allSources.add(it) + } + } + + text.isJsonArray() -> GSON.fromJsonArray(text).getOrThrow().let { items -> + allSources.addAll(items) + } + + text.isAbsUrl() -> { + importSourceUrl(text) + } + + text.isUri() -> { + importSourceAwait(text.toUri().readText(appCtx)) + } + + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().text().let { + importSourceAwait(it) + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { + val source = appDb.dictRuleDao.getByName(it.name) + checkSources.add(source) + selectStatus.add(source == null) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportHttpTtsDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportHttpTtsDialog.kt new file mode 100755 index 000000000..2ddee1f47 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportHttpTtsDialog.kt @@ -0,0 +1,187 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.HttpTTS +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + +class ImportHttpTtsDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + CodeDialog.Callback { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_tts) + binding.rotateLoading.visible() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: HttpTTS, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.name + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when { + localSource == null -> "新增" + item.lastUpdateTime > localSource.lastUpdateTime -> "更新" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } + + override fun onCodeSave(code: String, requestId: String?) { + requestId?.toInt()?.let { + HttpTTS.fromJson(code).getOrNull()?.let { source -> + viewModel.allSources[it] = source + adapter.setItem(it, source) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt new file mode 100755 index 000000000..3c8247595 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportHttpTtsViewModel.kt @@ -0,0 +1,127 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.readText +import splitties.init.appCtx + +class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) { + + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + selectSource.add(allSources[index]) + } + } + appDb.httpTTSDao.insert(*selectSource.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + importSourceAwait(text.trim()) + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceAwait(text: String) { + when { + text.isJsonObject() -> { + HttpTTS.fromJson(text).getOrThrow().let { + allSources.add(it) + } + } + + text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow().let { items -> + allSources.addAll(items) + } + + text.isAbsUrl() -> { + importSourceUrl(text) + } + + text.isUri() -> { + importSourceAwait(text.toUri().readText(appCtx)) + } + + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().text().let { + importSourceAwait(it) + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { + val source = appDb.httpTTSDao.get(it.id) + checkSources.add(source) + selectStatus.add(source == null || source.lastUpdateTime < it.lastUpdateTime) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt new file mode 100755 index 000000000..f12c59480 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt @@ -0,0 +1,253 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.DialogCustomGroupBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.dpToPx +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + +class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener, + CodeDialog.Callback { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_replace_rule) + binding.rotateLoading.visible() + initMenu() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allRules) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.import(source) + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.import_replace) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_new_group -> alertCustomGroup(item) + R.id.menu_keep_original_name -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepName, item.isChecked) + } + } + return true + } + + private fun alertCustomGroup(item: MenuItem) { + alert(R.string.diy_edit_source_group) { + val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply { + val groups = appDb.replaceRuleDao.allGroups() + textInputLayout.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { + alertBinding.root + } + okButton { + viewModel.isAddGroup = alertBinding.swAddGroup.isChecked + viewModel.groupName = alertBinding.editView.text?.toString() + if (viewModel.groupName.isNullOrBlank()) { + item.title = getString(R.string.diy_source_group) + } else { + val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName) + if (viewModel.isAddGroup) { + item.title = "+$group" + } else { + item.title = group + } + } + } + noButton() + } + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allRules.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allRules.size + ) + } + } + + override fun onCodeSave(code: String, requestId: String?) { + requestId?.toInt()?.let { + GSON.fromJsonObject(code).getOrNull()?.let { rule -> + viewModel.allRules[it] = rule + adapter.setItem(it, rule) + } + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + @SuppressLint("SetTextI18n") + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: ReplaceRule, + payloads: MutableList + ) { + binding.run { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = if (item.group.isNullOrBlank()) { + item.name + } else { + "${item.name}(${item.group})" + } + val localRule = viewModel.checkRules[holder.layoutPosition] + tvSourceState.text = when { + localRule == null -> "新增" + item.pattern != localRule.pattern + || item.replacement != localRule.replacement + || item.isRegex != localRule.isRegex + || item.scope != localRule.scope -> "更新" + + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.run { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allRules[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt new file mode 100755 index 000000000..9f7380680 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt @@ -0,0 +1,141 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.ReplaceAnalyzer +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.readText +import io.legado.app.utils.splitNotBlank +import splitties.init.appCtx + +class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { + var isAddGroup = false + var groupName: String? = null + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allRules = arrayListOf() + val checkRules = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val group = groupName?.trim() + val selectRules = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + val rule = allRules[index] + if (!group.isNullOrEmpty()) { + if (isAddGroup) { + val groups = linkedSetOf() + rule.group?.splitNotBlank(AppPattern.splitGroupRegex)?.let { + groups.addAll(it) + } + groups.add(group) + rule.group = groups.joinToString(",") + } else { + rule.group = group + } + } + selectRules.add(rule) + } + } + appDb.replaceRuleDao.insert(*selectRules.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + fun import(text: String) { + execute { + importAwait(text.trim()) + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importAwait(text: String) { + when { + text.isAbsUrl() -> importUrl(text) + text.isJsonArray() -> { + val rules = ReplaceAnalyzer.jsonToReplaceRules(text).getOrThrow() + allRules.addAll(rules) + } + + text.isJsonObject() -> { + val rule = ReplaceAnalyzer.jsonToReplaceRule(text).getOrThrow() + allRules.add(rule) + } + + text.isUri() -> { + importAwait(text.toUri().readText(appCtx)) + } + + else -> throw NoStackTraceException("格式不对") + } + } + + private suspend fun importUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().text("utf-8").let { + importAwait(it) + } + } + + private fun comparisonSource() { + execute { + allRules.forEach { + val rule = appDb.replaceRuleDao.findById(it.id) + checkRules.add(rule) + selectStatus.add(rule == null) + } + }.onSuccess { + successLiveData.postValue(allRules.size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt new file mode 100755 index 000000000..66d1d4e80 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt @@ -0,0 +1,266 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.DialogCustomGroupBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.dpToPx +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + +/** + * 导入rss源弹出窗口 + */ +class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener, + CodeDialog.Callback { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_rss_source) + binding.rotateLoading.visible() + initMenu() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.import_source) + binding.toolBar.menu.findItem(R.id.menu_keep_original_name)?.isChecked = + AppConfig.importKeepName + binding.toolBar.menu.findItem(R.id.menu_keep_group)?.isChecked = + AppConfig.importKeepGroup + binding.toolBar.menu.findItem(R.id.menu_keep_enable)?.isChecked = + AppConfig.importKeepEnable + binding.toolBar.menu.findItem(R.id.menu_select_new_source)?.isVisible = false + binding.toolBar.menu.findItem(R.id.menu_select_update_source)?.isVisible = false + } + + @SuppressLint("InflateParams") + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_new_group -> alertCustomGroup(item) + R.id.menu_keep_original_name -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepName, item.isChecked) + } + + R.id.menu_keep_group -> { + item.isChecked = !item.isChecked + putPrefBoolean(PreferKey.importKeepGroup, item.isChecked) + } + + R.id.menu_keep_enable -> { + item.isChecked = !item.isChecked + AppConfig.importKeepEnable = item.isChecked + } + } + return false + } + + private fun alertCustomGroup(item: MenuItem) { + alert(R.string.diy_edit_source_group) { + val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply { + val groups = appDb.rssSourceDao.allGroups() + textInputLayout.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { + alertBinding.root + } + okButton { + viewModel.isAddGroup = alertBinding.swAddGroup.isChecked + viewModel.groupName = alertBinding.editView.text?.toString() + if (viewModel.groupName.isNullOrBlank()) { + item.title = getString(R.string.diy_source_group) + } else { + val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName) + if (viewModel.isAddGroup) { + item.title = "+$group" + } else { + item.title = group + } + } + } + cancelButton() + } + } + + override fun onCodeSave(code: String, requestId: String?) { + requestId?.toInt()?.let { + GSON.fromJsonObject(code).getOrNull()?.let { source -> + viewModel.allSources[it] = source + adapter.setItem(it, source) + } + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: RssSource, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.sourceName + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when { + localSource == null -> "新增" + item.lastUpdateTime > localSource.lastUpdateTime -> "更新" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt new file mode 100755 index 000000000..fa27c2a5d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt @@ -0,0 +1,193 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData +import com.jayway.jsonpath.JsonPath +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.jsonPath +import io.legado.app.utils.readText +import io.legado.app.utils.splitNotBlank +import splitties.init.appCtx + +class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { + var isAddGroup = false + var groupName: String? = null + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val group = groupName?.trim() + val keepName = AppConfig.importKeepName + val keepGroup = AppConfig.importKeepGroup + val keepEnable = AppConfig.importKeepEnable + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + val source = allSources[index] + checkSources[index]?.let { + if (keepName) { + source.sourceName = it.sourceName + } + if (keepGroup) { + source.sourceGroup = it.sourceGroup + } + if (keepEnable) { + source.enabled = it.enabled + } + source.customOrder = it.customOrder + } + if (!group.isNullOrEmpty()) { + if (isAddGroup) { + val groups = linkedSetOf() + source.sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.let { + groups.addAll(it) + } + groups.add(group) + source.sourceGroup = groups.joinToString(",") + } else { + source.sourceGroup = group + } + } + selectSource.add(source) + } + } + SourceHelp.insertRssSource(*selectSource.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + importSourceAwait(text) + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceAwait(text: String) { + val mText = text.trim() + when { + mText.isJsonObject() -> kotlin.runCatching { + val json = JsonPath.parse(mText) + val urls = json.read>("$.sourceUrls") + if (!urls.isNullOrEmpty()) { + urls.forEach { + importSourceUrl(it) + } + } + }.onFailure { + GSON.fromJsonArray(mText).getOrThrow().let { + val source = it.firstOrNull() ?: return@let + if (source.sourceUrl.isEmpty()) { + throw NoStackTraceException("不是订阅源") + } + allSources.addAll(it) + } + } + + mText.isJsonArray() -> { + GSON.fromJsonArray(mText).getOrThrow().let { + val source = it.firstOrNull() ?: return@let + if (source.sourceUrl.isEmpty()) { + throw NoStackTraceException("不是订阅源") + } + allSources.addAll(it) + } + } + + mText.isAbsUrl() -> { + importSourceUrl(mText) + } + + mText.isUri() -> { + importSourceAwait(mText.toUri().readText(appCtx)) + } + + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().byteStream().use { body -> + val items: List> = jsonPath.parse(body).read("$") + for (item in items) { + if (!item.containsKey("sourceUrl")) { + throw NoStackTraceException("不是订阅源") + } + val jsonItem = jsonPath.parse(item) + GSON.fromJsonObject(jsonItem.jsonString()).getOrThrow().let { source -> + allSources.add(source) + } + } + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { + val has = appDb.rssSourceDao.getByKey(it.sourceUrl) + checkSources.add(has) + selectStatus.add(has == null || has.lastUpdateTime < it.lastUpdateTime) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportThemeDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportThemeDialog.kt new file mode 100755 index 000000000..ba9930d84 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportThemeDialog.kt @@ -0,0 +1,177 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.help.config.ThemeConfig +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + +class ImportThemeDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_theme) + binding.rotateLoading.visible() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: ThemeConfig.Config, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.themeName + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when { + localSource == null -> "新增" + localSource != item -> "更新" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportThemeViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportThemeViewModel.kt new file mode 100755 index 000000000..4f4afd8e7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportThemeViewModel.kt @@ -0,0 +1,130 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.ThemeConfig +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.readText +import splitties.init.appCtx + +class ImportThemeViewModel(app: Application) : BaseViewModel(app) { + + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + selectStatus.forEachIndexed { index, b -> + if (b) { + ThemeConfig.addConfig(allSources[index]) + } + } + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + importSourceAwait(text.trim()) + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceAwait(text: String) { + when { + text.isJsonObject() -> { + GSON.fromJsonObject(text).getOrThrow().let { + allSources.add(it) + } + } + + text.isJsonArray() -> GSON.fromJsonArray(text).getOrThrow() + .let { items -> + allSources.addAll(items) + } + + text.isAbsUrl() -> { + importSourceUrl(text) + } + + text.isUri() -> { + importSourceAwait(text.toUri().readText(appCtx)) + } + + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().text().let { + importSourceAwait(it) + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { config -> + val source = ThemeConfig.configList.find { + it.themeName == config.themeName + } + checkSources.add(source) + selectStatus.add(source == null || source != config) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleDialog.kt new file mode 100755 index 000000000..bdd9792dd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleDialog.kt @@ -0,0 +1,177 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemSourceImportBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.CodeDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.views.onClick + +class ImportTxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) { + + constructor(source: String, finishOnDismiss: Boolean = false) : this() { + arguments = Bundle().apply { + putString("source", source) + putBoolean("finishOnDismiss", finishOnDismiss) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { SourcesAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (arguments?.getBoolean("finishOnDismiss") == true) { + activity?.finish() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.import_txt_toc_rule) + binding.rotateLoading.visible() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importSelect { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + val selectAll = viewModel.isSelectAll + viewModel.selectStatus.forEachIndexed { index, b -> + if (b != !selectAll) { + viewModel.selectStatus[index] = !selectAll + } + } + adapter.notifyDataSetChanged() + upSelectText() + } + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allSources) + upSelectText() + } else { + binding.tvMsg.apply { + setText(R.string.wrong_format) + visible() + } + } + } + val source = arguments?.getString("source") + if (source.isNullOrEmpty()) { + dismiss() + return + } + viewModel.importSource(source) + } + + private fun upSelectText() { + if (viewModel.isSelectAll) { + binding.tvFooterLeft.text = getString( + R.string.select_cancel_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } else { + binding.tvFooterLeft.text = getString( + R.string.select_all_count, + viewModel.selectCount, + viewModel.allSources.size + ) + } + } + + inner class SourcesAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding { + return ItemSourceImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSourceImportBinding, + item: TxtTocRule, + payloads: MutableList + ) { + binding.apply { + cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] + cbSourceName.text = item.name + val localSource = viewModel.checkSources[holder.layoutPosition] + tvSourceState.text = when { + localSource == null -> "新增" + item != localSource -> "更新" + else -> "已有" + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSourceImportBinding) { + binding.apply { + cbSourceName.setOnUserCheckedChangeListener { isChecked -> + viewModel.selectStatus[holder.layoutPosition] = isChecked + upSelectText() + } + root.onClick { + cbSourceName.isChecked = !cbSourceName.isChecked + viewModel.selectStatus[holder.layoutPosition] = cbSourceName.isChecked + upSelectText() + } + tvOpen.setOnClickListener { + val source = viewModel.allSources[holder.layoutPosition] + showDialogFragment( + CodeDialog( + GSON.toJson(source), + disableEdit = false, + requestId = holder.layoutPosition.toString() + ) + ) + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleViewModel.kt new file mode 100755 index 000000000..0921cb00d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportTxtTocRuleViewModel.kt @@ -0,0 +1,131 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isUri +import io.legado.app.utils.readText +import splitties.init.appCtx + +class ImportTxtTocRuleViewModel(app: Application) : BaseViewModel(app) { + + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + + val allSources = arrayListOf() + val checkSources = arrayListOf() + val selectStatus = arrayListOf() + + val isSelectAll: Boolean + get() { + selectStatus.forEach { + if (!it) { + return false + } + } + return true + } + + val selectCount: Int + get() { + var count = 0 + selectStatus.forEach { + if (it) { + count++ + } + } + return count + } + + fun importSelect(finally: () -> Unit) { + execute { + val selectSource = arrayListOf() + selectStatus.forEachIndexed { index, b -> + if (b) { + selectSource.add(allSources[index]) + } + } + appDb.txtTocRuleDao.insert(*selectSource.toTypedArray()) + }.onFinally { + finally.invoke() + } + } + + fun importSource(text: String) { + execute { + importSourceAwait(text.trim()) + }.onError { + errorLiveData.postValue("ImportError:${it.localizedMessage}") + AppLog.put("ImportError:${it.localizedMessage}", it) + }.onSuccess { + comparisonSource() + } + } + + private suspend fun importSourceAwait(text: String) { + when { + text.isJsonObject() -> { + GSON.fromJsonObject(text).getOrThrow().let { + allSources.add(it) + } + } + + text.isJsonArray() -> GSON.fromJsonArray(text).getOrThrow() + .let { items -> + allSources.addAll(items) + } + + text.isAbsUrl() -> { + importSourceUrl(text) + } + + text.isUri() -> { + importSourceAwait(text.toUri().readText(appCtx)) + } + + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private suspend fun importSourceUrl(url: String) { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().text().let { + importSourceAwait(it) + } + } + + private fun comparisonSource() { + execute { + allSources.forEach { + val source = appDb.txtTocRuleDao.get(it.id) + checkSources.add(source) + selectStatus.add(source == null || it != source) + } + successLiveData.postValue(allSources.size) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt b/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt new file mode 100755 index 000000000..7d777464f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OnLineImportActivity.kt @@ -0,0 +1,115 @@ +package io.legado.app.ui.association + +import android.os.Bundle +import androidx.activity.viewModels +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 网络一键导入 + * 格式: legado://import/{path}?src={url} + */ +class OnLineImportActivity : + VMBaseActivity() { + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + override val viewModel by viewModels() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.successLive.observe(this) { + when (it.first) { + "bookSource" -> showDialogFragment( + ImportBookSourceDialog(it.second, true) + ) + "rssSource" -> showDialogFragment( + ImportRssSourceDialog(it.second, true) + ) + "replaceRule" -> showDialogFragment( + ImportReplaceRuleDialog(it.second, true) + ) + "httpTts" -> showDialogFragment( + ImportHttpTtsDialog(it.second, true) + ) + "theme" -> showDialogFragment( + ImportThemeDialog(it.second, true) + ) + "txtRule" -> showDialogFragment( + ImportTxtTocRuleDialog(it.second, true) + ) + "dictRule" -> showDialogFragment( + ImportDictRuleDialog(it.second, true) + ) + } + } + viewModel.errorLive.observe(this) { + finallyDialog(getString(R.string.error), it) + } + intent.data?.let { + val url = it.getQueryParameter("src") + if (url.isNullOrEmpty()) { + finish() + return + } + when (it.path) { + "/bookSource" -> showDialogFragment( + ImportBookSourceDialog(url, true) + ) + + "/rssSource" -> showDialogFragment( + ImportRssSourceDialog(url, true) + ) + + "/replaceRule" -> showDialogFragment( + ImportReplaceRuleDialog(url, true) + ) + "/textTocRule" -> showDialogFragment( + ImportTxtTocRuleDialog(url, true) + ) + "/httpTTS" -> showDialogFragment( + ImportHttpTtsDialog(url, true) + ) + "/dictRule" -> showDialogFragment( + ImportDictRuleDialog(url, true) + ) + "/theme" -> showDialogFragment( + ImportThemeDialog(url, true) + ) + "/readConfig" -> viewModel.getBytes(url) { bytes -> + viewModel.importReadConfig(bytes, this::finallyDialog) + } + "/addToBookshelf" -> showDialogFragment( + AddToBookshelfDialog(url, true) + ) + "/importonline" -> when (it.host) { + "booksource" -> showDialogFragment( + ImportBookSourceDialog(url, true) + ) + "rsssource" -> showDialogFragment( + ImportRssSourceDialog(url, true) + ) + "replace" -> showDialogFragment( + ImportReplaceRuleDialog(url, true) + ) + else -> { + viewModel.determineType(url, this::finallyDialog) + } + } + else -> viewModel.determineType(url, this::finallyDialog) + } + } + } + + private fun finallyDialog(title: String, msg: String) { + alert(title, msg) { + okButton() + onDismiss { + finish() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt b/app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt new file mode 100755 index 000000000..d95aac5ec --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OnLineImportViewModel.kt @@ -0,0 +1,111 @@ +package io.legado.app.ui.association + +import android.app.Application +import androidx.core.net.toUri +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.http.decompressed +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.utils.FileUtils +import io.legado.app.utils.externalCache +import okhttp3.MediaType.Companion.toMediaType +import splitties.init.appCtx + +class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) { + + fun getText(url: String, success: (text: String) -> Unit) { + execute { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.decompressed().text("utf-8") + }.onSuccess { + success.invoke(it) + }.onError { + errorLive.postValue( + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun getBytes(url: String, success: (bytes: ByteArray) -> Unit) { + execute { + okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + }.bytes() + }.onSuccess { + success.invoke(it) + }.onError { + errorLive.postValue( + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun importReadConfig(bytes: ByteArray, finally: (title: String, msg: String) -> Unit) { + execute { + val config = ReadBookConfig.import(bytes) + ReadBookConfig.configList.forEachIndexed { index, c -> + if (c.name == config.name) { + ReadBookConfig.configList[index] = config + return@execute config.name + } + ReadBookConfig.configList.add(config) + return@execute config.name + } + }.onSuccess { + finally.invoke(context.getString(R.string.success), "导入排版成功") + }.onError { + finally.invoke( + context.getString(R.string.error), + it.localizedMessage ?: context.getString(R.string.unknown_error) + ) + } + } + + fun determineType(url: String, finally: (title: String, msg: String) -> Unit) { + execute { + val rs = okHttpClient.newCallResponseBody { + if (url.endsWith("#requestWithoutUA")) { + url(url.substringBeforeLast("#requestWithoutUA")) + header(AppConst.UA_NAME, "null") + } else { + url(url) + } + } + when (rs.contentType()) { + "application/zip".toMediaType(), + "application/octet-stream".toMediaType() -> { + importReadConfig(rs.bytes(), finally) + } + else -> { + val inputStream = rs.byteStream() + val file = FileUtils.createFileIfNotExist( + appCtx.externalCache, + "download", + "scheme_import_cache.json" + ) + file.outputStream().use { out -> + inputStream.use { + it.copyTo(out) + } + } + importJson(file.toUri()) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmActivity.kt b/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmActivity.kt new file mode 100755 index 000000000..b9efa3c86 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmActivity.kt @@ -0,0 +1,25 @@ +package io.legado.app.ui.association + +import android.os.Bundle +import io.legado.app.base.BaseActivity +import io.legado.app.constant.SourceType +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class OpenUrlConfirmActivity : + BaseActivity() { + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + intent.getStringExtra("uri")?.let { + val mimeType = intent.getStringExtra("mimeType") + val sourceOrigin = intent.getStringExtra("sourceOrigin") + val sourceName = intent.getStringExtra("sourceName") + val sourceType = intent.getIntExtra("sourceType", SourceType.book) + showDialogFragment(OpenUrlConfirmDialog(it, mimeType, sourceOrigin, sourceName, sourceType)) + } ?: finish() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmDialog.kt b/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmDialog.kt new file mode 100755 index 000000000..41b316d37 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmDialog.kt @@ -0,0 +1,132 @@ +package io.legado.app.ui.association + +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.core.net.toUri +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppLog +import io.legado.app.databinding.DialogOpenUrlConfirmBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.init.appCtx + +class OpenUrlConfirmDialog() : BaseDialogFragment(R.layout.dialog_open_url_confirm), + Toolbar.OnMenuItemClickListener { + + constructor( + uri: String, + mimeType: String?, + sourceOrigin: String? = null, + sourceName: String? = null, + sourceType: Int + ) : this() { + arguments = Bundle().apply { + putString("uri", uri) + putString("mimeType", mimeType) + putString("sourceOrigin", sourceOrigin) + putString("sourceName", sourceName) + putInt("sourceType", sourceType) + } + } + + val binding by viewBinding(DialogOpenUrlConfirmBinding::bind) + val viewModel by viewModels() + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initMenu() + val arguments = arguments ?: return + viewModel.initData(arguments) + if (viewModel.uri.isBlank()) { + dismiss() + return + } + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.subtitle = viewModel.sourceName + initView() + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.open_url_confirm) + binding.toolBar.menu.applyTint(requireContext()) + } + + private fun initView() { + binding.message.text = "${viewModel.sourceName} 正在请求跳转链接/应用,是否跳转?" + binding.btnNegative.setOnClickListener { dismiss() } + binding.btnPositive.setOnClickListener { + openUrl() + dismiss() + } + } + + private fun openUrl() { + try { + val uri = viewModel.uri.toUri() + val mimeType = viewModel.mimeType + // 创建目标 Intent 并设置类型 + val targetIntent = Intent(Intent.ACTION_VIEW).apply { + // 同时设置 Data 和 Type + if (!mimeType.isNullOrBlank()) { + setDataAndType(uri, mimeType) + } else { + data = uri + } + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + // 验证是否有应用可以处理 + if (targetIntent.resolveActivity(appCtx.packageManager) != null) { + startActivity(targetIntent) + } else { + toastOnUi(R.string.can_not_open) + } + } catch (e: Exception) { + AppLog.put("打开链接失败", e, true) + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_disable_source -> { + viewModel.disableSource { + dismiss() + } + } + + R.id.menu_delete_source -> { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + viewModel.sourceName) + noButton() + yesButton { + viewModel.deleteSource { + dismiss() + } + } + } + } + } + return false + } + + override fun onDestroy() { + super.onDestroy() + activity?.finish() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmViewModel.kt b/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmViewModel.kt new file mode 100755 index 000000000..65db30882 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/OpenUrlConfirmViewModel.kt @@ -0,0 +1,42 @@ +package io.legado.app.ui.association + +import android.app.Application +import android.os.Bundle +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.SourceType +import io.legado.app.data.appDb +import io.legado.app.help.source.SourceHelp + +class OpenUrlConfirmViewModel(app: Application): BaseViewModel(app) { + + var uri = "" + var mimeType: String? = null + var sourceOrigin = "" + var sourceName = "" + var sourceType = SourceType.book + + fun initData(arguments: Bundle) { + uri = arguments.getString("uri") ?: "" + mimeType = arguments.getString("mimeType") + sourceName = arguments.getString("sourceName") ?: "" + sourceOrigin = arguments.getString("sourceOrigin") ?: "" + sourceType = arguments.getInt("sourceType", SourceType.book) + } + + fun disableSource(block: () -> Unit) { + execute { + SourceHelp.enableSource(sourceOrigin, sourceType, false) + }.onSuccess { + block.invoke() + } + } + + fun deleteSource(block: () -> Unit) { + execute { + SourceHelp.deleteSource(sourceOrigin, sourceType) + }.onSuccess { + block.invoke() + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/association/VerificationCodeActivity.kt b/app/src/main/java/io/legado/app/ui/association/VerificationCodeActivity.kt new file mode 100755 index 000000000..a70a5f8ae --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/VerificationCodeActivity.kt @@ -0,0 +1,29 @@ +package io.legado.app.ui.association + +import android.os.Bundle +import io.legado.app.base.BaseActivity +import io.legado.app.constant.SourceType +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 验证码 + */ +class VerificationCodeActivity : + BaseActivity() { + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + intent.getStringExtra("imageUrl")?.let { + val sourceOrigin = intent.getStringExtra("sourceOrigin") + val sourceName = intent.getStringExtra("sourceName") + val sourceType = intent.getIntExtra("sourceType", SourceType.book) + showDialogFragment( + VerificationCodeDialog(it, sourceOrigin, sourceName, sourceType) + ) + } ?: finish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt b/app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt new file mode 100755 index 000000000..c9c901f27 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt @@ -0,0 +1,156 @@ +package io.legado.app.ui.association + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogVerificationCodeViewBinding +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.help.source.SourceVerificationHelp +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.ImageProvider +import io.legado.app.ui.widget.dialog.PhotoDialog +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 图片验证码对话框 + * 结果保存在内存中 + * val key = "${sourceOrigin ?: ""}_verificationResult" + * CacheManager.get(key) + */ +class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view), + Toolbar.OnMenuItemClickListener { + + constructor( + imageUrl: String, + sourceOrigin: String? = null, + sourceName: String? = null, + sourceType: Int + ) : this() { + arguments = Bundle().apply { + putString("imageUrl", imageUrl) + putString("sourceOrigin", sourceOrigin) + putString("sourceName", sourceName) + putInt("sourceType", sourceType) + } + } + + val binding by viewBinding(DialogVerificationCodeViewBinding::bind) + val viewModel by viewModels() + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + private var sourceOrigin: String? = null + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?): Unit = binding.run { + initMenu() + val arguments = arguments ?: return@run + viewModel.initData(arguments) + toolBar.setBackgroundColor(primaryColor) + toolBar.subtitle = arguments.getString("sourceName") + sourceOrigin = arguments.getString("sourceOrigin") + val imageUrl = arguments.getString("imageUrl") ?: return@run + loadImage(imageUrl, sourceOrigin) + verificationCodeImageView.setOnClickListener { + showDialogFragment(PhotoDialog(imageUrl, sourceOrigin)) + } + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.verification_code) + binding.toolBar.menu.applyTint(requireContext()) + } + + @SuppressLint("CheckResult") + private fun loadImage(url: String, sourceUrl: String?) { + ImageProvider.remove(url) + ImageLoader.loadBitmap(requireContext(), url).apply { + sourceUrl?.let { + apply(RequestOptions().set(OkHttpModelLoader.sourceOriginOption, it)) + } + }.error(R.drawable.image_loading_error) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Bitmap, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + val bitmap = resource.copy(resource.config!!, true) + ImageProvider.put(url, bitmap) // 传给 PhotoDialog + return false + } + }) + .into(binding.verificationCodeImageView) + } + + @SuppressLint("InflateParams") + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_ok -> { + val verificationCode = binding.verificationCode.text.toString() + SourceVerificationHelp.setResult(sourceOrigin!!, verificationCode) + dismiss() + } + + R.id.menu_disable_source -> { + viewModel.disableSource { + dismiss() + } + } + + R.id.menu_delete_source -> { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + viewModel.sourceName) + noButton() + yesButton { + viewModel.deleteSource { + dismiss() + } + } + } + } + } + return false + } + + override fun onDestroy() { + SourceVerificationHelp.checkResult(sourceOrigin!!) + super.onDestroy() + activity?.finish() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/association/VerificationCodeViewModel.kt b/app/src/main/java/io/legado/app/ui/association/VerificationCodeViewModel.kt new file mode 100755 index 000000000..3bb29170d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/VerificationCodeViewModel.kt @@ -0,0 +1,37 @@ +package io.legado.app.ui.association + +import android.app.Application +import android.os.Bundle +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.SourceType +import io.legado.app.help.source.SourceHelp + +class VerificationCodeViewModel(app: Application): BaseViewModel(app) { + + var sourceOrigin = "" + var sourceName = "" + private var sourceType = SourceType.book + + fun initData(arguments: Bundle) { + sourceName = arguments.getString("sourceName") ?: "" + sourceOrigin = arguments.getString("sourceOrigin") ?: "" + sourceType = arguments.getInt("sourceType", SourceType.book) + } + + fun disableSource(block: () -> Unit) { + execute { + SourceHelp.enableSource(sourceOrigin, sourceType, false) + }.onSuccess { + block.invoke() + } + } + + fun deleteSource(block: () -> Unit) { + execute { + SourceHelp.deleteSource(sourceOrigin, sourceType) + }.onSuccess { + block.invoke() + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt new file mode 100755 index 000000000..81889b0a7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt @@ -0,0 +1,313 @@ +package io.legado.app.ui.book.audio + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.view.Gravity +import android.view.Menu +import android.view.MenuItem +import android.widget.SeekBar +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.constant.Status +import io.legado.app.constant.Theme +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.ActivityAudioPlayBinding +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.removeType +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.model.AudioPlay +import io.legado.app.model.BookCover +import io.legado.app.service.AudioPlayService +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.changesource.ChangeBookSourceDialog +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.dpToPx +import io.legado.app.utils.invisible +import io.legado.app.utils.observeEvent +import io.legado.app.utils.observeEventSticky +import io.legado.app.utils.sendToClip +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.toDurationTime +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.views.onLongClick +import java.util.Locale + +/** + * 音频播放 + */ +@SuppressLint("ObsoleteSdkInt") +class AudioPlayActivity : + VMBaseActivity(toolBarTheme = Theme.Dark), + ChangeBookSourceDialog.CallBack, + AudioPlay.CallBack { + + override val binding by viewBinding(ActivityAudioPlayBinding::inflate) + override val viewModel by viewModels() + private val timerSliderPopup by lazy { TimerSliderPopup(this) } + private var adjustProgress = false + private var playMode = AudioPlay.PlayMode.LIST_END_STOP + + private val tocActivityResult = registerForActivityResult(TocActivityResult()) { + it?.let { + if (it.first != AudioPlay.book?.durChapterIndex + || it.second == 0 + ) { + AudioPlay.skipTo(it.first) + } + } + } + private val sourceEditResult = + registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) { + if (it.resultCode == RESULT_OK) { + viewModel.upSource() + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.titleBar.setBackgroundResource(R.color.transparent) + AudioPlay.register(this) + viewModel.titleData.observe(this) { + binding.titleBar.title = it + } + viewModel.coverData.observe(this) { + upCover(it) + } + viewModel.initData(intent) + initView() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.audio_play, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_login)?.isVisible = !AudioPlay.bookSource?.loginUrl.isNullOrBlank() + menu.findItem(R.id.menu_wake_lock)?.isChecked = AppConfig.audioPlayUseWakeLock + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_change_source -> AudioPlay.book?.let { + showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) + } + + R.id.menu_login -> AudioPlay.bookSource?.let { + startActivity { + putExtra("type", "bookSource") + putExtra("key", it.bookSourceUrl) + } + } + + R.id.menu_wake_lock -> AppConfig.audioPlayUseWakeLock = !AppConfig.audioPlayUseWakeLock + R.id.menu_copy_audio_url -> sendToClip(AudioPlayService.url) + R.id.menu_edit_source -> AudioPlay.bookSource?.let { + sourceEditResult.launch { + putExtra("sourceUrl", it.bookSourceUrl) + } + } + + R.id.menu_log -> showDialogFragment() + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + binding.ivPlayMode.setOnClickListener { + AudioPlay.changePlayMode() + } + + observeEventSticky(EventBus.PLAY_MODE_CHANGED) { + playMode = it + updatePlayModeIcon() + } + + binding.fabPlayStop.setOnClickListener { + playButton() + } + binding.fabPlayStop.onLongClick { + AudioPlay.stop() + } + binding.ivSkipNext.setOnClickListener { + AudioPlay.next() + } + binding.ivSkipPrevious.setOnClickListener { + AudioPlay.prev() + } + binding.playerProgress.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + binding.tvDurTime.text = progress.toDurationTime() + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + adjustProgress = true + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + adjustProgress = false + AudioPlay.adjustProgress(seekBar.progress) + } + }) + binding.ivChapter.setOnClickListener { + AudioPlay.book?.let { + tocActivityResult.launch(it.bookUrl) + } + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + binding.ivFastRewind.invisible() + binding.ivFastForward.invisible() + } + binding.ivFastForward.setOnClickListener { + AudioPlay.adjustSpeed(0.1f) + } + binding.ivFastRewind.setOnClickListener { + AudioPlay.adjustSpeed(-0.1f) + } + binding.ivTimer.setOnClickListener { + timerSliderPopup.showAsDropDown(it, 0, (-100).dpToPx(), Gravity.TOP) + } + binding.llPlayMenu.applyNavigationBarPadding() + } + + private fun updatePlayModeIcon() { + binding.ivPlayMode.setImageResource(playMode.iconRes) + } + + private fun upCover(path: String?) { + BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) { + BookCover.loadBlur(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) + .into(binding.ivBg) + }.into(binding.ivCover) + } + + private fun playButton() { + when (AudioPlay.status) { + Status.PLAY -> AudioPlay.pause(this) + Status.PAUSE -> AudioPlay.resume(this) + else -> AudioPlay.loadOrUpPlayUrl() + } + } + + override val oldBook: Book? + get() = AudioPlay.book + + override fun changeTo(source: BookSource, book: Book, toc: List) { + if (book.isAudio) { + viewModel.changeTo(source, book, toc) + } else { + AudioPlay.stop() + lifecycleScope.launch { + withContext(IO) { + AudioPlay.book?.migrateTo(book, toc) + book.removeType(BookType.updateError) + AudioPlay.book?.delete() + appDb.bookDao.insert(book) + } + startActivityForBook(book) + finish() + } + } + } + + override fun finish() { + val book = AudioPlay.book ?: return super.finish() + + if (AudioPlay.inBookshelf) { + return super.finish() + } + + if (!AppConfig.showAddToShelfAlert) { + viewModel.removeFromBookshelf { super.finish() } + } else { + alert(title = getString(R.string.add_to_bookshelf)) { + setMessage(getString(R.string.check_add_bookshelf, book.name)) + okButton { + AudioPlay.book?.removeType(BookType.notShelf) + AudioPlay.book?.save() + AudioPlay.inBookshelf = true + setResult(RESULT_OK) + } + noButton { viewModel.removeFromBookshelf { super.finish() } } + } + } + } + + override fun onDestroy() { + super.onDestroy() + if (AudioPlay.status != Status.PLAY) { + AudioPlay.stop() + } + AudioPlay.unregister(this) + } + + @SuppressLint("SetTextI18n") + override fun observeLiveBus() { + observeEvent(EventBus.MEDIA_BUTTON) { + if (it) { + playButton() + } + } + observeEventSticky(EventBus.AUDIO_STATE) { + AudioPlay.status = it + if (it == Status.PLAY) { + binding.fabPlayStop.setImageResource(R.drawable.ic_pause_24dp) + } else { + binding.fabPlayStop.setImageResource(R.drawable.ic_play_24dp) + } + } + observeEventSticky(EventBus.AUDIO_SUB_TITLE) { + binding.tvSubTitle.text = it + binding.ivSkipPrevious.isEnabled = AudioPlay.durChapterIndex > 0 + binding.ivSkipNext.isEnabled = + AudioPlay.durChapterIndex < AudioPlay.simulatedChapterSize - 1 + } + observeEventSticky(EventBus.AUDIO_SIZE) { + binding.playerProgress.max = it + binding.tvAllTime.text = it.toDurationTime() + } + observeEventSticky(EventBus.AUDIO_PROGRESS) { + if (!adjustProgress) binding.playerProgress.progress = it + binding.tvDurTime.text = it.toDurationTime() + } + observeEventSticky(EventBus.AUDIO_BUFFER_PROGRESS) { + binding.playerProgress.secondaryProgress = it + + } + observeEventSticky(EventBus.AUDIO_SPEED) { + binding.tvSpeed.text = String.format(Locale.ROOT, "%.1fX", it) + binding.tvSpeed.visible() + } + observeEventSticky(EventBus.AUDIO_DS) { + binding.tvTimer.text = "${it}m" + binding.tvTimer.visible(it > 0) + } + } + + override fun upLoading(loading: Boolean) { + runOnUiThread { + binding.progressLoading.visible(loading) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt new file mode 100755 index 000000000..1fcbd85e7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt @@ -0,0 +1,120 @@ +package io.legado.app.ui.book.audio + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.help.book.getBookSource +import io.legado.app.help.book.removeType +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.model.AudioPlay +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi + +class AudioPlayViewModel(application: Application) : BaseViewModel(application) { + val titleData = MutableLiveData() + val coverData = MutableLiveData() + + fun initData(intent: Intent) = AudioPlay.apply { + execute { + val bookUrl = intent.getStringExtra("bookUrl") ?: book?.bookUrl ?: return@execute + val book = appDb.bookDao.getBook(bookUrl) ?: return@execute + inBookshelf = intent.getBooleanExtra("inBookshelf", true) + initBook(book) + }.onFinally { + saveRead() + } + } + + private suspend fun initBook(book: Book) { + val isSameBook = AudioPlay.book?.bookUrl == book.bookUrl + if (isSameBook) { + AudioPlay.upData(book) + } else { + AudioPlay.resetData(book) + } + titleData.postValue(book.name) + coverData.postValue(book.getDisplayCover()) + if (book.tocUrl.isEmpty() && !loadBookInfo(book)) { + return + } + if (AudioPlay.chapterSize == 0 && !loadChapterList(book)) { + return + } + } + + private suspend fun loadBookInfo(book: Book): Boolean { + val bookSource = AudioPlay.bookSource ?: return true + try { + WebBook.getBookInfoAwait(bookSource, book) + return true + } catch (e: Exception) { + AppLog.put("详情页出错: ${e.localizedMessage}", e, true) + return false + } + } + + private suspend fun loadChapterList(book: Book): Boolean { + val bookSource = AudioPlay.bookSource ?: return true + try { + val oldBook = book.copy() + val cList = WebBook.getChapterListAwait(bookSource, book).getOrThrow() + if (oldBook.bookUrl == book.bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + } + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*cList.toTypedArray()) + AudioPlay.chapterSize = cList.size + AudioPlay.simulatedChapterSize = book.simulatedTotalChapterNum() + AudioPlay.upDurChapter() + return true + } catch (e: Exception) { + context.toastOnUi(R.string.error_load_toc) + return false + } + } + + fun upSource() { + execute { + val book = AudioPlay.book ?: return@execute + AudioPlay.bookSource = book.getBookSource() + } + } + + fun changeTo(source: BookSource, book: Book, toc: List) { + execute { + AudioPlay.book?.migrateTo(book, toc) + book.removeType(BookType.updateError) + AudioPlay.book?.delete() + appDb.bookDao.insert(book) + AudioPlay.book = book + AudioPlay.bookSource = source + appDb.bookChapterDao.insert(*toc.toTypedArray()) + AudioPlay.upDurChapter() + }.onFinally { + postEvent(EventBus.SOURCE_CHANGED, book.bookUrl) + } + } + + fun removeFromBookshelf(success: (() -> Unit)?) { + execute { + AudioPlay.book?.let { + appDb.bookDao.delete(it) + } + }.onSuccess { + success?.invoke() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/audio/TimerSliderPopup.kt b/app/src/main/java/io/legado/app/ui/book/audio/TimerSliderPopup.kt new file mode 100755 index 000000000..f8025039f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/audio/TimerSliderPopup.kt @@ -0,0 +1,55 @@ +package io.legado.app.ui.book.audio + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import android.widget.SeekBar +import io.legado.app.R +import io.legado.app.databinding.PopupSeekBarBinding +import io.legado.app.model.AudioPlay +import io.legado.app.service.AudioPlayService +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener + +class TimerSliderPopup(private val context: Context) : + PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) { + + private val binding = PopupSeekBarBinding.inflate(LayoutInflater.from(context)) + + init { + contentView = binding.root + + isTouchable = true + isOutsideTouchable = false + isFocusable = true + + binding.seekBar.max = 180 + setProcessTextValue(binding.seekBar.progress) + binding.seekBar.setOnSeekBarChangeListener(object : SeekBarChangeListener { + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + setProcessTextValue(progress) + if (fromUser) { + AudioPlay.setTimer(progress) + } + } + + }) + } + + override fun showAsDropDown(anchor: View?, xoff: Int, yoff: Int, gravity: Int) { + super.showAsDropDown(anchor, xoff, yoff, gravity) + binding.seekBar.progress = AudioPlayService.timeMinute + } + + override fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) { + super.showAtLocation(parent, gravity, x, y) + binding.seekBar.progress = AudioPlayService.timeMinute + } + + private fun setProcessTextValue(process: Int) { + binding.tvSeekValue.text = context.getString(R.string.timer_m, process) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkActivity.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkActivity.kt new file mode 100755 index 000000000..dce48e0e8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkActivity.kt @@ -0,0 +1,82 @@ +package io.legado.app.ui.book.bookmark + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ActivityAllBookmarkBinding +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 所有书签 + */ +class AllBookmarkActivity : VMBaseActivity(), + BookmarkAdapter.Callback { + + override val viewModel by viewModels() + override val binding by viewBinding(ActivityAllBookmarkBinding::inflate) + private val adapter by lazy { + BookmarkAdapter(this, this) + } + private val exportDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + when (it.requestCode) { + 1 -> viewModel.exportBookmark(uri) + 2 -> viewModel.exportBookmarkMd(uri) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + lifecycleScope.launch { + appDb.bookmarkDao.flowAll().catch { + AppLog.put("所有书签界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + private fun initView() { + binding.recyclerView.addItemDecoration(BookmarkDecoration(adapter)) + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.bookmark, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_export -> exportDir.launch { + requestCode = 1 + } + + R.id.menu_export_md -> exportDir.launch { + requestCode = 2 + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onItemClick(bookmark: Bookmark, position: Int) { + showDialogFragment(BookmarkDialog(bookmark, position)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt new file mode 100755 index 000000000..b2557d3b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt @@ -0,0 +1,67 @@ +package io.legado.app.ui.book.bookmark + +import android.app.Application +import android.net.Uri +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.utils.FileDoc +import io.legado.app.utils.GSON +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeToOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class AllBookmarkViewModel(application: Application) : BaseViewModel(application) { + + + /** + * 导出书签 + */ + fun exportBookmark(treeUri: Uri) { + execute { + val dateFormat = SimpleDateFormat("yyMMddHHmmss", Locale.getDefault()) + val fileName = "bookmark-${dateFormat.format(Date())}.json" + val dirDoc = FileDoc.fromUri(treeUri, true) + dirDoc.createFileIfNotExist(fileName).openOutputStream().getOrThrow().use { + GSON.writeToOutputStream(it, appDb.bookmarkDao.all) + } + }.onError { + AppLog.put("导出失败\n${it.localizedMessage}", it, true) + }.onSuccess { + context.toastOnUi("导出成功") + } + } + + + fun exportBookmarkMd(treeUri: Uri) { + execute { + val dateFormat = SimpleDateFormat("yyMMddHHmmss", Locale.getDefault()) + val fileName = "bookmark-${dateFormat.format(Date())}.md" + val dirDoc = FileDoc.fromUri(treeUri, true) + val fileDoc = dirDoc.createFileIfNotExist(fileName).openOutputStream().getOrThrow() + fileDoc.use { outputStream -> + var name = "" + var author = "" + appDb.bookmarkDao.all.forEach { + if (it.bookName != name && it.bookAuthor != author) { + name = it.bookName + author = it.bookAuthor + outputStream.write("## ${it.bookName} ${it.bookAuthor}\n\n".toByteArray()) + } + outputStream.write("#### ${it.chapterName}\n\n".toByteArray()) + outputStream.write("###### 原文\n ${it.bookText}\n\n".toByteArray()) + outputStream.write("###### 摘要\n ${it.content}\n\n".toByteArray()) + } + } + }.onError { + AppLog.put("导出失败\n${it.localizedMessage}", it, true) + }.onSuccess { + context.toastOnUi("导出成功") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkAdapter.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkAdapter.kt new file mode 100755 index 000000000..a823c082e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkAdapter.kt @@ -0,0 +1,60 @@ +package io.legado.app.ui.book.bookmark + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ItemBookmarkBinding +import io.legado.app.utils.gone +import splitties.views.onClick + +class BookmarkAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookmarkBinding { + return ItemBookmarkBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookmarkBinding, + item: Bookmark, + payloads: MutableList + ) { + binding.tvChapterName.text = item.chapterName + binding.tvBookText.gone(item.bookText.isEmpty()) + binding.tvBookText.text = item.bookText + binding.tvContent.gone(item.content.isEmpty()) + binding.tvContent.text = item.content + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookmarkBinding) { + binding.root.onClick { + getItemByLayoutPosition(holder.layoutPosition)?.let { + callback.onItemClick(it, holder.layoutPosition) + } + } + } + + fun getHeaderText(position: Int): String { + return with(getItem(position)) { + "${this?.bookName ?: ""}(${this?.bookAuthor ?: ""})" + } + } + + fun isItemHeader(position: Int): Boolean { + if (position == 0) return true + val lastItem = getItem(position - 1) + val curItem = getItem(position) + return !(lastItem?.bookName == curItem?.bookName + && lastItem?.bookAuthor == curItem?.bookAuthor) + } + + interface Callback { + + fun onItemClick(bookmark: Bookmark, position: Int) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDecoration.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDecoration.kt new file mode 100755 index 000000000..136245ef9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDecoration.kt @@ -0,0 +1,111 @@ +package io.legado.app.ui.book.bookmark + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.text.TextPaint +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.utils.dpToPx +import io.legado.app.utils.spToPx +import splitties.init.appCtx +import kotlin.math.min + +class BookmarkDecoration(val adapter: BookmarkAdapter) : RecyclerView.ItemDecoration() { + + private val headerLeft = 16f.dpToPx() + private val headerHeight = 32f.dpToPx() + + private val headerPaint = Paint().apply { + color = appCtx.backgroundColor + } + private val textPaint = TextPaint().apply { + textSize = 16f.spToPx() + color = appCtx.accentColor + isAntiAlias = true + } + private val textRect = Rect() + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val count = parent.childCount + for (i in 0 until count) { + val view = parent.getChildAt(i) + val position = parent.getChildLayoutPosition(view) + val isHeader = adapter.isItemHeader(position) + if (isHeader) { + c.drawRect( + 0f, + view.top - headerHeight, + parent.width.toFloat(), + view.top.toFloat(), + headerPaint + ) + val headerText = adapter.getHeaderText(position) + textPaint.getTextBounds(headerText, 0, headerText.length, textRect) + c.drawText( + headerText, + headerLeft, + (view.top - headerHeight) + headerHeight / 2 + textRect.height() / 2, + textPaint + ) + } + } + } + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val position = (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + val view = parent.findViewHolderForAdapterPosition(position)?.itemView ?: return + val isHeader = adapter.isItemHeader(position + 1) + val headerText = adapter.getHeaderText(position) + if (isHeader) { + val bottom = min(headerHeight.toInt(), view.bottom) + c.drawRect( + 0f, + view.top - headerHeight, + parent.width.toFloat(), + bottom.toFloat(), + headerPaint + ) + textPaint.getTextBounds(headerText, 0, headerText.length, textRect) + c.drawText( + headerText, + headerLeft, + headerHeight / 2 + textRect.height() / 2 - (headerHeight - bottom), + textPaint + ) + } else { + c.drawRect( + 0f, + 0f, + parent.width.toFloat(), + headerHeight, + headerPaint + ) + textPaint.getTextBounds(headerText, 0, headerText.length, textRect) + c.drawText( + headerText, + headerLeft, + headerHeight / 2 + textRect.height() / 2, + textPaint + ) + } + c.save() + } + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildLayoutPosition(view) + val isHeader = adapter.isItemHeader(position) + if (isHeader) { + outRect.top = headerHeight.toInt() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDialog.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDialog.kt new file mode 100755 index 000000000..25d7f31d2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/BookmarkDialog.kt @@ -0,0 +1,79 @@ +package io.legado.app.ui.book.bookmark + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.appDb +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.DialogBookmarkBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class BookmarkDialog() : BaseDialogFragment(R.layout.dialog_bookmark, true) { + + constructor(bookmark: Bookmark, editPos: Int = -1) : this() { + arguments = Bundle().apply { + putInt("editPos", editPos) + putParcelable("bookmark", bookmark) + } + } + + private val binding by viewBinding(DialogBookmarkBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + val arguments = arguments ?: let { + dismiss() + return + } + + @Suppress("DEPRECATION") + val bookmark = arguments.getParcelable("bookmark") + bookmark ?: let { + dismiss() + return + } + val editPos = arguments.getInt("editPos", -1) + binding.tvFooterLeft.visible(editPos >= 0) + binding.run { + tvChapterName.text = bookmark.chapterName + editBookText.setText(bookmark.bookText) + editContent.setText(bookmark.content) + tvCancel.setOnClickListener { + dismiss() + } + tvOk.setOnClickListener { + bookmark.bookText = editBookText.text?.toString() ?: "" + bookmark.content = editContent.text?.toString() ?: "" + lifecycleScope.launch { + withContext(IO) { + appDb.bookmarkDao.insert(bookmark) + } + dismiss() + } + } + tvFooterLeft.setOnClickListener { + lifecycleScope.launch { + withContext(IO) { + appDb.bookmarkDao.delete(bookmark) + } + dismiss() + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt new file mode 100755 index 000000000..409312120 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt @@ -0,0 +1,581 @@ +package io.legado.app.ui.book.cache + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.textfield.TextInputLayout +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst.charsets +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ActivityCacheBookBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogSelectSectionExportBinding +import io.legado.app.help.book.getExportFileName +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.tryParesExportFileName +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.model.CacheBook +import io.legado.app.service.ExportBookService +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.ACache +import io.legado.app.utils.FileDoc +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.applyOpenTint +import io.legado.app.utils.applyTint +import io.legado.app.utils.checkWrite +import io.legado.app.utils.cnCompare +import io.legado.app.utils.enableCustomExport +import io.legado.app.utils.flowWithLifecycleAndDatabaseChange +import io.legado.app.utils.iconItemOnLongClick +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.observeEvent +import io.legado.app.utils.setIconCompat +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startService +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.verificationField +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import kotlin.math.max + +/** + * cache/download 缓存界面 + */ +class CacheActivity : VMBaseActivity(), + PopupMenu.OnMenuItemClickListener, + CacheAdapter.CallBack { + + override val binding by viewBinding(ActivityCacheBookBinding::inflate) + override val viewModel by viewModels() + + private val exportBookPathKey = "exportBookPath" + private val exportTypes = arrayListOf("txt", "epub") + private val layoutManager by lazy { LinearLayoutManager(this) } + private val adapter by lazy { CacheAdapter(this, this) } + private var booksFlowJob: Job? = null + private var menu: Menu? = null + private val groupList: ArrayList = arrayListOf() + private var groupId: Long = -1 + + private val exportDir = registerForActivityResult(HandleFileContract()) { result -> + var isReadyPath = false + var dirPath = "" + result.uri?.let { uri -> + if (uri.isContentScheme()) { + ACache.get().put(exportBookPathKey, uri.toString()) + dirPath = uri.toString() + isReadyPath = true + } else { + uri.path?.let { path -> + ACache.get().put(exportBookPathKey, path) + dirPath = path + isReadyPath = true + } + } + } + if (!isReadyPath) { + return@registerForActivityResult + } + if (enableCustomExport()) {// 启用自定义导出 and 导出类型为Epub + configExportSection(dirPath, result.requestCode) + } else { + startExport(dirPath, result.requestCode) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + groupId = intent.getLongExtra("groupId", -1) + lifecycleScope.launch { + binding.titleBar.subtitle = withContext(IO) { + appDb.bookGroupDao.getByID(groupId)?.groupName + ?: getString(R.string.no_group) + } + } + initRecyclerView() + initGroupData() + initBookData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_cache, menu) + menu.iconItemOnLongClick(R.id.menu_download) { + PopupMenu(this, it).apply { + inflate(R.menu.book_cache_download) + this.menu.applyOpenTint(this@CacheActivity) + setOnMenuItemClickListener(this@CacheActivity) + }.show() + } + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace + // 菜单打开时读取状态[enableCustomExport] + menu.findItem(R.id.menu_enable_custom_export)?.isChecked = AppConfig.enableCustomExport + menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName + menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav + menu.findItem(R.id.menu_export_pics_file)?.isChecked = AppConfig.exportPictureFile + menu.findItem(R.id.menu_parallel_export)?.isChecked = AppConfig.parallelExportBook + menu.findItem(R.id.menu_export_type)?.title = + "${getString(R.string.export_type)}(${getTypeName()})" + menu.findItem(R.id.menu_export_charset)?.title = + "${getString(R.string.export_charset)}(${AppConfig.exportCharset})" + return super.onMenuOpened(featureId, menu) + } + + private fun upMenu() { + menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group) + groupList.forEach { bookGroup -> + subMenu.add(R.id.menu_group, bookGroup.order, Menu.NONE, bookGroup.groupName) + } + } + } + + /** + * 菜单按下回调 + */ + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_download, + R.id.menu_download_after -> { + if (!CacheBook.isRun) sureCacheBook { + adapter.getItems().forEach { book -> + CacheBook.start( + this@CacheActivity, + book, + book.durChapterIndex, + book.lastChapterIndex + ) + } + } else { + CacheBook.stop(this@CacheActivity) + } + } + + R.id.menu_download_all -> { + if (!CacheBook.isRun) sureCacheBook { + adapter.getItems().forEach { book -> + CacheBook.start( + this@CacheActivity, + book, + 0, + book.lastChapterIndex + ) + } + } else { + CacheBook.stop(this@CacheActivity) + } + } + + R.id.menu_export_all -> exportAll() + R.id.menu_enable_replace -> AppConfig.exportUseReplace = !item.isChecked + // 更改菜单状态[enableCustomExport] + R.id.menu_enable_custom_export -> AppConfig.enableCustomExport = !item.isChecked + R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked + R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked + R.id.menu_export_pics_file -> AppConfig.exportPictureFile = !item.isChecked + R.id.menu_parallel_export -> AppConfig.parallelExportBook = !item.isChecked + R.id.menu_export_folder -> { + selectExportFolder(-1) + } + + R.id.menu_export_file_name -> alertExportFileName() + R.id.menu_export_type -> showExportTypeConfig() + R.id.menu_export_charset -> showCharsetConfig() + R.id.menu_log -> showDialogFragment() + else -> if (item.groupId == R.id.menu_group) { + binding.titleBar.subtitle = item.title + groupId = appDb.bookGroupDao.getByName(item.title.toString())?.groupId ?: 0 + initBookData() + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + return onCompatOptionsItemSelected(item) + } + + private fun initRecyclerView() { + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + } + + private fun initBookData() { + booksFlowJob?.cancel() + booksFlowJob = lifecycleScope.launch { + appDb.bookDao.flowByGroup(groupId).map { books -> + val booksDownload = books.filter { + !it.isAudio + } + when (AppConfig.getBookSortByGroupId(groupId)) { + 1 -> booksDownload.sortedByDescending { it.latestChapterTime } + 2 -> booksDownload.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + + 3 -> booksDownload.sortedBy { it.order } + 4 -> booksDownload.sortedByDescending { + max(it.latestChapterTime, it.durChapterTime) + } + + else -> booksDownload.sortedByDescending { it.durChapterTime } + } + }.flowWithLifecycleAndDatabaseChange( + lifecycle, table = AppDatabase.BOOK_TABLE_NAME + ).catch { + AppLog.put("缓存管理界面获取书籍列表失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { books -> + adapter.setItems(books) + viewModel.loadCacheFiles(books) + } + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun initGroupData() { + lifecycleScope.launch { + appDb.bookGroupDao.flowAll().catch { + AppLog.put("缓存管理界面获取分组数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { + groupList.clear() + groupList.addAll(it) + adapter.notifyDataSetChanged() + upMenu() + } + } + } + + private fun notifyItemChanged(bookUrl: String) { + kotlin.runCatching { + adapter.getItems().forEachIndexed { index, book -> + if (bookUrl == book.bookUrl) { + adapter.notifyItemChanged(index, true) + return + } + } + } + } + + override fun observeLiveBus() { + viewModel.upAdapterLiveData.observe(this) { + notifyItemChanged(it) + } + observeEvent(EventBus.EXPORT_BOOK) { + notifyItemChanged(it) + } + observeEvent(EventBus.UP_DOWNLOAD) { + notifyItemChanged(it) + } + observeEvent(EventBus.UP_DOWNLOAD_STATE) { + if (!CacheBook.isRun) { + menu?.findItem(R.id.menu_download)?.let { item -> + item.setIconCompat(R.drawable.ic_play_24dp) + item.setTitle(R.string.download_start) + } + menu?.applyTint(this) + } else { + menu?.findItem(R.id.menu_download)?.let { item -> + item.setIconCompat(R.drawable.ic_stop_black_24dp) + item.setTitle(R.string.stop) + } + menu?.applyTint(this) + } + } + observeEvent>(EventBus.SAVE_CONTENT) { (book, chapter) -> + viewModel.cacheChapters[book.bookUrl]?.add(chapter.url) + notifyItemChanged(book.bookUrl) + } + } + + override fun export(position: Int) { + val path = ACache.get().getAsString(exportBookPathKey) + lifecycleScope.launch { + if (path.isNullOrEmpty() || + withContext(IO) { !FileDoc.fromDir(path).checkWrite() } + ) { + selectExportFolder(position) + } else if (enableCustomExport()) {// 启用自定义导出 and 导出类型为Epub + configExportSection(path, position) + } else { + startExport(path, position) + } + } + } + + private fun exportAll() { + val path = ACache.get().getAsString(exportBookPathKey) + if (path.isNullOrEmpty()) { + selectExportFolder(-10) + } else { + startExport(path, -10) + } + } + + /** + * 配置自定义导出对话框 + * + * @param path 导出路径 + * @param position book位置 + * @author Discut + * @since 1.0.0 + */ + private fun configExportSection(path: String, position: Int) { + + val alertBinding = DialogSelectSectionExportBinding.inflate(layoutInflater) + .apply { + fun verifyExportFileNameJsStr(js: String): Boolean { + return tryParesExportFileName(js) && etEpubFilename.text.toString() + .isNotEmpty() + } + + fun enableLyEtEpubFilenameIcon() { + lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_CUSTOM + lyEtEpubFilename.setEndIconOnClickListener { + adapter.getItem(position)?.run { + lyEtEpubFilename.helperText = + if (verifyExportFileNameJsStr(etEpubFilename.text.toString())) + "${resources.getString(R.string.result_analyzed)}: ${ + getExportFileName( + "epub", + 1, + etEpubFilename.text.toString() + ) + }" + else "Error" + } ?: run { + lyEtEpubFilename.helperText = "Error" + AppLog.put("未找到书籍,position is $position") + } + } + } + etEpubSize.setText("1") + // lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_NONE + etEpubFilename.text?.append(AppConfig.episodeExportFileName) + // 存储解析文件名的jsStr + etEpubFilename.let { + it.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) + return@setOnFocusChangeListener + it.text?.run { + if (verifyExportFileNameJsStr(toString())) { + AppConfig.episodeExportFileName = toString() + } + } + } + } + tvAllExport.setOnClickListener { + cbAllExport.callOnClick() + } + tvSelectExport.setOnClickListener { + cbSelectExport.callOnClick() + } + cbSelectExport.onCheckedChangeListener = { _, isChecked -> + if (isChecked) { + etEpubSize.isEnabled = true + etInputScope.isEnabled = true + etEpubFilename.isEnabled = true + enableLyEtEpubFilenameIcon() + cbAllExport.isChecked = false + } + } + cbAllExport.onCheckedChangeListener = { _, isChecked -> + if (isChecked) { + etEpubSize.isEnabled = false + etInputScope.isEnabled = false + etEpubFilename.isEnabled = false + lyEtEpubFilename.endIconMode = TextInputLayout.END_ICON_NONE + cbSelectExport.isChecked = false + } + } + + etInputScope.onFocusChangeListener = + View.OnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + etInputScope.hint = "1-5,8,10-18" + } else { + etInputScope.hint = "" + } + } + + // 默认选择自定义导出 + cbSelectExport.callOnClick() + } + val alertDialog = alert(titleResource = R.string.select_section_export) { + customView { alertBinding.root } + positiveButton(R.string.ok) + cancelButton() + } + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + alertBinding.apply { + if (cbAllExport.isChecked) { + startExport(path, position) + alertDialog.hide() + return@apply + } + val epubScope = etInputScope.text.toString() + if (!verificationField(epubScope)) { + etInputScope.error = appCtx.getString(R.string.error_scope_input)//"请输入正确的范围" + return@apply + } + etInputScope.error = null + val epubSize = etEpubSize.text.toString().toIntOrNull() ?: 1 + adapter.getItem(position)?.let { book -> + startService { + action = IntentAction.start + putExtra("bookUrl", book.bookUrl) + putExtra("exportType", "epub") + putExtra("exportPath", path) + putExtra("epubSize", epubSize) + putExtra("epubScope", epubScope) + } + } + alertDialog.hide() + } + + } + } + + private fun selectExportFolder(exportPosition: Int) { + val default = arrayListOf>() + val path = ACache.get().getAsString(exportBookPathKey) + if (!path.isNullOrEmpty()) { + default.add(SelectItem(path, -1)) + } + exportDir.launch { + otherActions = default + requestCode = exportPosition + } + } + + private fun startExport(path: String, exportPosition: Int) { + val exportType = when (AppConfig.exportType) { + 1 -> "epub" + else -> "txt" + } + if (exportPosition == -10) { + if (adapter.getItems().isNotEmpty()) { + adapter.getItems().forEach { book -> + startService { + action = IntentAction.start + putExtra("bookUrl", book.bookUrl) + putExtra("exportType", exportType) + putExtra("exportPath", path) + } + } + } else { + toastOnUi(R.string.no_book) + } + } else if (exportPosition >= 0) { + adapter.getItem(exportPosition)?.let { book -> + startService { + action = IntentAction.start + putExtra("bookUrl", book.bookUrl) + putExtra("exportType", exportType) + putExtra("exportPath", path) + } + } + } + } + + @SuppressLint("SetTextI18n") + private fun alertExportFileName() { + alert(R.string.export_file_name) { + val message = "Variable: name, author." + setMessage(message) + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "file name js" + editView.setText(AppConfig.bookExportFileName) + } + customView { alertBinding.root } + okButton { + AppConfig.bookExportFileName = alertBinding.editView.text?.toString() + } + cancelButton() + } + } + + private fun getTypeName(): String { + return exportTypes.getOrElse(AppConfig.exportType) { + exportTypes[0] + } + } + + private fun showExportTypeConfig() { + selector(R.string.export_type, exportTypes) { _, i -> + AppConfig.exportType = i + } + } + + private fun showCharsetConfig() { + alert(R.string.set_charset) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "charset name" + editView.setFilterValues(charsets) + editView.setText(AppConfig.exportCharset) + } + customView { alertBinding.root } + okButton { + AppConfig.exportCharset = alertBinding.editView.text?.toString() ?: "UTF-8" + } + cancelButton() + } + } + + private fun sureCacheBook(action: () -> Unit) { + alert(R.string.draw) { + setMessage(R.string.sure_cache_book) + noButton() + yesButton { + action.invoke() + } + } + } + + override val cacheChapters: HashMap> + get() = viewModel.cacheChapters + + override fun exportProgress(bookUrl: String): Int? { + return ExportBookService.exportProgress[bookUrl] + } + + override fun exportMsg(bookUrl: String): String? { + return ExportBookService.exportMsg[bookUrl] + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt new file mode 100755 index 000000000..5d3049440 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt @@ -0,0 +1,141 @@ +package io.legado.app.ui.book.cache + +import android.content.Context +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ItemDownloadBinding +import io.legado.app.help.book.isLocal +import io.legado.app.model.CacheBook +import io.legado.app.utils.gone +import io.legado.app.utils.visible + +class CacheAdapter(context: Context, private val callBack: CallBack) : + DiffRecyclerAdapter(context) { + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean { + return oldItem.bookUrl == newItem.bookUrl + } + + override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean { + return oldItem.name == newItem.name + && oldItem.author == newItem.author + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemDownloadBinding { + return ItemDownloadBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemDownloadBinding, + item: Book, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + tvName.text = item.name + tvAuthor.text = context.getString(R.string.author_show, item.getRealAuthor()) + if (item.isLocal) { + tvDownload.setText(R.string.local_book) + } else { + val cs = callBack.cacheChapters[item.bookUrl] + if (cs == null) { + tvDownload.setText(R.string.loading) + } else { + tvDownload.text = + context.getString( + R.string.download_count, + cs.size, + item.totalChapterNum + ) + } + } + } else { + if (item.isLocal) { + tvDownload.setText(R.string.local_book) + } else { + val cacheSize = callBack.cacheChapters[item.bookUrl]?.size ?: 0 + tvDownload.text = + context.getString(R.string.download_count, cacheSize, item.totalChapterNum) + } + } + upDownloadIv(ivDownload, item) + upExportInfo(tvMsg, progressExport, item) + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemDownloadBinding) { + binding.run { + ivDownload.setOnClickListener { + getItem(holder.layoutPosition)?.let { book -> + CacheBook.cacheBookMap[book.bookUrl]?.let { + if (!it.isStop()) { + CacheBook.remove(context, book.bookUrl) + } else { + CacheBook.start(context, book, 0, book.lastChapterIndex) + } + } ?: let { + CacheBook.start(context, book, 0, book.lastChapterIndex) + } + } + } + tvExport.setOnClickListener { + callBack.export(holder.layoutPosition) + } + } + } + + private fun upDownloadIv(iv: ImageView, book: Book) { + if (book.isLocal) { + iv.gone() + } else { + iv.visible() + CacheBook.cacheBookMap[book.bookUrl]?.let { + if (!it.isStop()) { + iv.setImageResource(R.drawable.ic_stop_black_24dp) + } else { + iv.setImageResource(R.drawable.ic_play_24dp) + } + } ?: let { + iv.setImageResource(R.drawable.ic_play_24dp) + } + } + } + + private fun upExportInfo(msgView: TextView, progressView: ProgressBar, book: Book) { + val msg = callBack.exportMsg(book.bookUrl) + if (msg != null) { + msgView.text = msg + msgView.visible() + progressView.gone() + return + } + msgView.gone() + val progress = callBack.exportProgress(book.bookUrl) + if (progress != null) { + progressView.max = book.totalChapterNum + progressView.progress = progress + progressView.visible() + return + } + progressView.gone() + } + + interface CallBack { + val cacheChapters: HashMap> + fun export(position: Int) + fun exportProgress(bookUrl: String): Int? + fun exportMsg(bookUrl: String): String? + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt new file mode 100755 index 000000000..2453879c0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt @@ -0,0 +1,46 @@ +package io.legado.app.ui.book.cache + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isLocal +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.sendValue +import kotlinx.coroutines.ensureActive +import kotlin.collections.set + + +class CacheViewModel(application: Application) : BaseViewModel(application) { + val upAdapterLiveData = MutableLiveData() + + private var loadChapterCoroutine: Coroutine? = null + val cacheChapters = hashMapOf>() + + fun loadCacheFiles(books: List) { + loadChapterCoroutine?.cancel() + loadChapterCoroutine = execute { + books.forEach { book -> + if (!book.isLocal && !cacheChapters.contains(book.bookUrl)) { + val chapterCaches = hashSetOf() + val cacheNames = BookHelp.getChapterFiles(book) + if (cacheNames.isNotEmpty()) { + appDb.bookChapterDao.getChapterList(book.bookUrl).also { + book.totalChapterNum = it.size + }.forEach { chapter -> + if (cacheNames.contains(chapter.getFileName()) || chapter.isVolume) { + chapterCaches.add(chapter.url) + } + } + } + cacheChapters[book.bookUrl] = chapterCaches + upAdapterLiveData.sendValue(book.bookUrl) + } + ensureActive() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt new file mode 100755 index 000000000..258cf97f6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt @@ -0,0 +1,116 @@ +package io.legado.app.ui.book.changecover + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogChangeCoverBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * 换封面 + */ +class ChangeCoverDialog() : BaseDialogFragment(R.layout.dialog_change_cover), + Toolbar.OnMenuItemClickListener, + CoverAdapter.CallBack { + + constructor(name: String, author: String) : this() { + arguments = Bundle().apply { + putString("name", name) + putString("author", author) + } + } + + private val binding by viewBinding(DialogChangeCoverBinding::bind) + private val callBack: CallBack? get() = activity as? CallBack + private val viewModel: ChangeCoverViewModel by viewModels() + private val adapter by lazy { CoverAdapter(requireContext(), this) } + + private val startStopMenuItem: MenuItem? + get() = binding.toolBar.menu.findItem(R.id.menu_start_stop) + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.change_cover_source) + viewModel.initData(arguments) + initMenu() + initView() + initData() + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.change_cover) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + } + + private fun initView() { + binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 3) + binding.recyclerView.adapter = adapter + } + + private fun initData() { + lifecycleScope.launch { + lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) } + viewModel.dataFlow.conflate().collect { + adapter.setItems(it) + delay(1000) + } + } + } + + override fun observeLiveBus() { + super.observeLiveBus() + viewModel.searchStateData.observe(viewLifecycleOwner) { + binding.refreshProgressBar.isAutoLoading = it + if (it) { + startStopMenuItem?.let { item -> + item.setIcon(R.drawable.ic_stop_black_24dp) + item.setTitle(R.string.stop) + } + } else { + startStopMenuItem?.let { item -> + item.setIcon(R.drawable.ic_refresh_black_24dp) + item.setTitle(R.string.refresh) + } + } + binding.toolBar.menu.applyTint(requireContext()) + } + + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_start_stop -> viewModel.startOrStopSearch() + } + return false + } + + override fun changeTo(coverUrl: String) { + callBack?.coverChangeTo(coverUrl) + dismissAllowingStateLoss() + } + + interface CallBack { + fun coverChangeTo(coverUrl: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt new file mode 100755 index 000000000..2df4a0b5c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt @@ -0,0 +1,170 @@ +package io.legado.app.ui.book.changecover + +import android.app.Application +import android.os.Bundle +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.SearchBook +import io.legado.app.help.config.AppConfig +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.mapParallelSafe +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import java.util.Collections +import java.util.concurrent.Executors +import kotlin.math.min + +class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { + private val threadCount = AppConfig.threadCount + private var searchPool: ExecutorCoroutineDispatcher? = null + private var searchSuccess: ((SearchBook) -> Unit)? = null + private var upAdapter: (() -> Unit)? = null + private var bookSourceParts = arrayListOf() + private val defaultCover by lazy { + listOf( + SearchBook( + originName = "默认封面", + name = name, + author = author, + coverUrl = "use_default_cover" + ) + ) + } + private var task: Job? = null + val searchStateData = MutableLiveData() + var name: String = "" + var author: String = "" + val searchBooks: MutableList = Collections.synchronizedList(arrayListOf()) + val dataFlow = callbackFlow { + + searchSuccess = { searchBook -> + if (!searchBooks.contains(searchBook)) { + searchBooks.add(searchBook) + trySend(defaultCover + searchBooks.sortedBy { it.originOrder }) + } + } + + upAdapter = { + trySend(defaultCover + searchBooks.sortedBy { it.originOrder }) + } + + appDb.searchBookDao.getEnableHasCover(name, author).let { + searchBooks.addAll(it) + trySend(defaultCover + searchBooks.toList()) + } + + if (searchBooks.size <= 1) { + startSearch() + } + + awaitClose { + searchBooks.clear() + searchSuccess = null + upAdapter = null + } + }.flowOn(IO) + + fun initData(arguments: Bundle?) { + arguments?.let { bundle -> + bundle.getString("name")?.let { + name = it + } + bundle.getString("author")?.let { + author = it.replace(AppPattern.authorRegex, "") + } + } + } + + private fun initSearchPool() { + searchPool = Executors + .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() + } + + private fun startSearch() { + execute { + stopSearch() + searchBooks.clear() + upAdapter?.invoke() + bookSourceParts.clear() + bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart) + initSearchPool() + search() + } + } + + private fun search() { + task = viewModelScope.launch(searchPool!!) { + flow { + for (bs in bookSourceParts) { + bs.getBookSource()?.let { + emit(it) + } + } + }.onStart { + searchStateData.postValue(true) + }.mapParallelSafe(threadCount) { + withTimeout(60000L) { + search(it) + } + }.onCompletion { + searchStateData.postValue(false) + }.catch { + AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it) + }.collect() + } + } + + private suspend fun search(source: BookSource) { + if (source.getSearchRule().coverUrl.isNullOrBlank()) { + return + } + val searchBook = WebBook.searchBookAwait( + source, name, + shouldBreak = { it > 0 }).firstOrNull() ?: return + if (searchBook.name == name && searchBook.author == author + && !searchBook.coverUrl.isNullOrEmpty() + ) { + appDb.searchBookDao.insert(searchBook) + searchSuccess?.invoke(searchBook) + } + } + + fun startOrStopSearch() { + if (task == null || !task!!.isActive) { + startSearch() + } else { + stopSearch() + } + } + + private fun stopSearch() { + task?.cancel() + searchPool?.close() + searchStateData.postValue(false) + } + + override fun onCleared() { + super.onCleared() + searchPool?.close() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt new file mode 100755 index 000000000..92b90ac94 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changecover/CoverAdapter.kt @@ -0,0 +1,55 @@ +package io.legado.app.ui.book.changecover + +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemCoverBinding + + +class CoverAdapter(context: Context, val callBack: CallBack) : + DiffRecyclerAdapter(context) { + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.bookUrl == newItem.bookUrl + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.originName == newItem.originName + && oldItem.coverUrl == newItem.coverUrl + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemCoverBinding { + return ItemCoverBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemCoverBinding, + item: SearchBook, + payloads: MutableList + ) = binding.run { + ivCover.load(item.coverUrl, item.name, item.author, false, item.origin) + tvSource.text = item.originName + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemCoverBinding) { + holder.itemView.apply { + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.changeTo(it.coverUrl ?: "") + } + } + } + } + + interface CallBack { + fun changeTo(coverUrl: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt new file mode 100755 index 000000000..644e1add3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt @@ -0,0 +1,236 @@ +package io.legado.app.ui.book.changesource + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemChangeSourceBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.visible +import splitties.init.appCtx +import splitties.views.onLongClick + + +class ChangeBookSourceAdapter( + context: Context, + val viewModel: ChangeBookSourceViewModel, + val callBack: CallBack +) : DiffRecyclerAdapter(context) { + + override val diffItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.bookUrl == newItem.bookUrl + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.originName == newItem.originName + && oldItem.getDisplayLastChapterTitle() == newItem.getDisplayLastChapterTitle() + && oldItem.chapterWordCountText == newItem.chapterWordCountText + && oldItem.respondTime == newItem.respondTime + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemChangeSourceBinding { + return ItemChangeSourceBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemChangeSourceBinding, + item: SearchBook, + payloads: MutableList + ) { + binding.apply { + if (payloads.isEmpty()) { + tvOrigin.text = item.originName + tvAuthor.text = item.author + tvLast.text = item.getDisplayLastChapterTitle() + tvCurrentChapterWordCount.text = item.chapterWordCountText + tvRespondTime.text = context.getString(R.string.respondTime, item.respondTime) + if (callBack.oldBookUrl == item.bookUrl) { + ivChecked.visible() + } else { + ivChecked.invisible() + } + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "name" -> tvOrigin.text = item.originName + "latest" -> tvLast.text = item.getDisplayLastChapterTitle() + "upCurSource" -> if (callBack.oldBookUrl == item.bookUrl) { + ivChecked.visible() + } else { + ivChecked.invisible() + } + } + } + } + } + val score = callBack.getBookScore(item) + if (score > 0) { + binding.ivBad.gone() + binding.ivGood.visible() + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_A200) + ) + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_100) + ) + } else if (score < 0) { + binding.ivGood.gone() + binding.ivBad.visible() + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_100) + ) + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_A200) + ) + } else { + binding.ivGood.visible() + binding.ivBad.visible() + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_100) + ) + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_100) + ) + } + + if (AppConfig.changeSourceLoadWordCount && !item.chapterWordCountText.isNullOrBlank()) { + tvCurrentChapterWordCount.visible() + } else { + tvCurrentChapterWordCount.gone() + } + + if (AppConfig.changeSourceLoadWordCount && item.respondTime >= 0) { + tvRespondTime.visible() + } else { + tvRespondTime.gone() + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) { + binding.ivGood.setOnClickListener { + if (binding.ivBad.isVisible) { + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_A200) + ) + binding.ivBad.gone() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, 1) + } + } else { + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_100) + ) + binding.ivBad.visible() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, 0) + } + } + } + binding.ivBad.setOnClickListener { + if (binding.ivGood.isVisible) { + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_A200) + ) + binding.ivGood.gone() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, -1) + } + } else { + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_100) + ) + binding.ivGood.visible() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, 0) + } + } + } + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (it.bookUrl != callBack.oldBookUrl) { + callBack.changeTo(it) + } + } + } + holder.itemView.onLongClick { + showMenu(holder.itemView, getItem(holder.layoutPosition)) + } + } + + private fun showMenu(view: View, searchBook: SearchBook?) { + searchBook ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.change_source_item) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_top_source -> { + callBack.topSource(searchBook) + } + + R.id.menu_bottom_source -> { + callBack.bottomSource(searchBook) + } + + R.id.menu_edit_source -> { + callBack.editSource(searchBook) + } + + R.id.menu_disable_source -> { + callBack.disableSource(searchBook) + } + + R.id.menu_delete_source -> context.alert(R.string.draw) { + setMessage(context.getString(R.string.sure_del) + "\n" + searchBook.originName) + noButton() + yesButton { + callBack.deleteSource(searchBook) + updateItems(0, itemCount, listOf()) + } + } + } + true + } + popupMenu.show() + } + + interface CallBack { + val oldBookUrl: String? + fun changeTo(searchBook: SearchBook) + fun topSource(searchBook: SearchBook) + fun bottomSource(searchBook: SearchBook) + fun editSource(searchBook: SearchBook) + fun disableSource(searchBook: SearchBook) + fun deleteSource(searchBook: SearchBook) + fun setBookScore(searchBook: SearchBook, score: Int) + fun getBookScore(searchBook: SearchBook): Int + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt new file mode 100755 index 000000000..7bc266f53 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt @@ -0,0 +1,462 @@ +package io.legado.app.ui.book.changesource + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.DialogBookChangeSourceBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.elevation +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatDrawable +import io.legado.app.utils.observeEvent +import io.legado.app.utils.setLayout +import io.legado.app.utils.startActivity +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * 换源界面 + */ +class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_source), + Toolbar.OnMenuItemClickListener, + ChangeBookSourceAdapter.CallBack { + + constructor(name: String, author: String) : this() { + arguments = Bundle().apply { + putString("name", name) + putString("author", author) + } + } + + private val binding by viewBinding(DialogBookChangeSourceBinding::bind) + private val groups = linkedSetOf() + private val callBack: CallBack? get() = activity as? CallBack + private val viewModel: ChangeBookSourceViewModel by viewModels() + private val waitDialog by lazy { WaitDialog(requireContext()) } + private val adapter by lazy { ChangeBookSourceAdapter(requireContext(), viewModel, this) } + private val editSourceResult = + registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) { + val origin = it.data?.getStringExtra("origin") ?: return@registerForActivityResult + viewModel.startSearch(origin) + } + private val searchFinishCallback: (isEmpty: Boolean) -> Unit = { + if (it) { + val searchGroup = AppConfig.searchGroup + if (searchGroup.isNotEmpty()) { + lifecycleScope.launch { + context?.alert("搜索结果为空") { + setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组") + cancelButton() + okButton { + AppConfig.searchGroup = "" + upGroupMenuName() + viewModel.startSearch() + } + } + } + } + } + } + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + viewModel.initData(arguments, callBack?.oldBook, activity is ReadBookActivity) + showTitle() + initMenu() + initRecyclerView() + initNavigationView() + initSearchView() + initBottomBar() + initLiveData() + viewModel.searchFinishCallback = searchFinishCallback + } + + override fun onDestroy() { + super.onDestroy() + viewModel.searchFinishCallback = null + } + + private fun showTitle() { + binding.toolBar.title = viewModel.name + binding.toolBar.subtitle = viewModel.author + binding.toolBar.navigationIcon = + getCompatDrawable(androidx.appcompat.R.drawable.abc_ic_ab_back_material) + binding.toolBar.elevation = requireContext().elevation + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.change_source) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.menu.findItem(R.id.menu_check_author) + ?.isChecked = AppConfig.changeSourceCheckAuthor + binding.toolBar.menu.findItem(R.id.menu_load_info) + ?.isChecked = AppConfig.changeSourceLoadInfo + binding.toolBar.menu.findItem(R.id.menu_load_toc) + ?.isChecked = AppConfig.changeSourceLoadToc + binding.toolBar.menu.findItem(R.id.menu_load_word_count) + ?.isChecked = AppConfig.changeSourceLoadWordCount + } + + private fun initRecyclerView() { + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0) { + binding.recyclerView.scrollToPosition(0) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + if (toPosition == 0) { + binding.recyclerView.scrollToPosition(0) + } + } + }) + } + + private fun initSearchView() { + val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView + searchView.setOnCloseListener { + showTitle() + false + } + searchView.setOnSearchClickListener { + binding.toolBar.title = "" + binding.toolBar.subtitle = "" + binding.toolBar.navigationIcon = null + } + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.screen(newText) + return false + } + + }) + } + + private fun initNavigationView() { + binding.toolBar.navigationIcon = + getCompatDrawable(androidx.appcompat.R.drawable.abc_ic_ab_back_material) + binding.toolBar.setNavigationContentDescription( + androidx.appcompat.R.string.abc_action_bar_up_description + ) + binding.toolBar.setNavigationOnClickListener { + dismissAllowingStateLoss() + } + kotlin.runCatching { + val mNavButtonViewField = Toolbar::class.java.getDeclaredField("mNavButtonView") + mNavButtonViewField.isAccessible = true + val navigationView = mNavButtonViewField.get(binding.toolBar) as ImageButton + val isLight = ColorUtils.isColorLight(primaryColor) + val textColor = requireContext().getPrimaryTextColor(isLight) + navigationView.setColorFilter(textColor) + } + } + + private fun initBottomBar() { + binding.tvDur.text = callBack?.oldBook?.originName + binding.tvDur.setOnClickListener { + scrollToDurSource() + } + binding.ivTop.setOnClickListener { + binding.recyclerView.scrollToPosition(0) + } + binding.ivBottom.setOnClickListener { + binding.recyclerView.scrollToPosition(adapter.itemCount - 1) + } + } + + private fun initLiveData() { + viewModel.searchStateData.observe(viewLifecycleOwner) { + binding.refreshProgressBar.isAutoLoading = it + if (it) { + startStopMenuItem?.let { item -> + item.setIcon(R.drawable.ic_stop_black_24dp) + item.setTitle(R.string.stop) + } + } else { + startStopMenuItem?.let { item -> + item.setIcon(R.drawable.ic_refresh_black_24dp) + item.setTitle(R.string.refresh) + } + } + binding.toolBar.menu.applyTint(requireContext()) + } + lifecycleScope.launch { + lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) } + viewModel.searchDataFlow.conflate().collect { + adapter.setItems(it) + delay(1000) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(STARTED) { + viewModel.changeSourceProgress + .drop(1) + .collect { (count, name) -> + binding.tvDur.text = + getString( + R.string.change_source_progress, + adapter.itemCount, + count, + viewModel.totalSourceCount, + name + ) + delay(500) + } + } + } + + lifecycleScope.launch { + appDb.bookSourceDao.flowEnabledGroups().conflate().collect { + groups.clear() + groups.addAll(it) + upGroupMenu() + } + } + } + + private val startStopMenuItem: MenuItem? + get() = binding.toolBar.menu.findItem(R.id.menu_start_stop) + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_check_author -> { + AppConfig.changeSourceCheckAuthor = !item.isChecked + item.isChecked = !item.isChecked + viewModel.refresh() + } + + R.id.menu_load_info -> { + AppConfig.changeSourceLoadInfo = !item.isChecked + item.isChecked = !item.isChecked + } + + R.id.menu_load_toc -> { + AppConfig.changeSourceLoadToc = !item.isChecked + item.isChecked = !item.isChecked + } + + R.id.menu_load_word_count -> { + AppConfig.changeSourceLoadWordCount = !item.isChecked + item.isChecked = !item.isChecked + viewModel.onLoadWordCountChecked(item.isChecked) + } + + R.id.menu_start_stop -> viewModel.startOrStopSearch() + R.id.menu_source_manage -> startActivity() + R.id.menu_close -> dismissAllowingStateLoss() + R.id.menu_refresh_list -> viewModel.startRefreshList() + else -> if (item?.groupId == R.id.source_group && !item.isChecked) { + item.isChecked = true + if (item.title.toString() == getString(R.string.all_source)) { + AppConfig.searchGroup = "" + } else { + AppConfig.searchGroup = item.title.toString() + } + upGroupMenuName() + lifecycleScope.launch(IO) { + viewModel.stopSearch() + if (viewModel.refresh()) { + viewModel.startSearch() + } + } + } + } + return false + } + + private fun scrollToDurSource() { + adapter.getItems().forEachIndexed { index, searchBook -> + if (searchBook.bookUrl == oldBookUrl) { + (binding.recyclerView.layoutManager as LinearLayoutManager) + .scrollToPositionWithOffset(index, 60.dpToPx()) + return + } + } + } + + override fun changeTo(searchBook: SearchBook) { + val oldBookType = callBack?.oldBook?.type ?: 0 + if (searchBook.sameBookTypeLocal(oldBookType)) { + changeSource(searchBook) { + dismissAllowingStateLoss() + } + } else { + alert( + titleResource = R.string.book_type_different, + messageResource = R.string.soure_change_source + ) { + okButton { + changeSource(searchBook) { + dismissAllowingStateLoss() + } + } + cancelButton() + } + } + } + + override val oldBookUrl: String? + get() = callBack?.oldBook?.bookUrl + + override fun topSource(searchBook: SearchBook) { + viewModel.topSource(searchBook) + } + + override fun bottomSource(searchBook: SearchBook) { + viewModel.bottomSource(searchBook) + } + + override fun editSource(searchBook: SearchBook) { + editSourceResult.launch { + putExtra("sourceUrl", searchBook.origin) + } + } + + override fun disableSource(searchBook: SearchBook) { + viewModel.disableSource(searchBook) + } + + override fun deleteSource(searchBook: SearchBook) { + viewModel.del(searchBook) + if (oldBookUrl == searchBook.bookUrl) { + viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source -> + callBack?.changeTo(source, book, toc) + } + } + } + + override fun setBookScore(searchBook: SearchBook, score: Int) { + viewModel.setBookScore(searchBook, score) + } + + override fun getBookScore(searchBook: SearchBook): Int { + return viewModel.getBookScore(searchBook) + } + + private fun changeSource(searchBook: SearchBook, onSuccess: (() -> Unit)? = null) { + waitDialog.setText(R.string.load_toc) + waitDialog.show() + val book = viewModel.bookMap[searchBook.primaryStr()] ?: searchBook.toBook() + val coroutine = viewModel.getToc(book, { toc, source -> + waitDialog.dismiss() + callBack?.changeTo(source, book, toc) + onSuccess?.invoke() + }, { + waitDialog.dismiss() + AppLog.put("换源获取目录出错\n$it", it, true) + }) + waitDialog.setOnCancelListener { + coroutine.cancel() + } + } + + /** + * 更新分组菜单 + */ + private fun upGroupMenu() { + binding.toolBar.menu.findItem(R.id.menu_group)?.run { + subMenu?.transaction { menu -> + val selectedGroup = AppConfig.searchGroup + menu.removeGroup(R.id.source_group) + val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) + var hasSelectedGroup = false + groups.forEach { group -> + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, group)?.let { + if (group == selectedGroup) { + it.isChecked = true + hasSelectedGroup = true + } + } + } + menu.setGroupCheckable(R.id.source_group, true, true) + if (hasSelectedGroup) { + title = getString(R.string.group) + "(" + AppConfig.searchGroup + ")" + } else { + allItem.isChecked = true + title = getString(R.string.group) + } + } + } + } + + /** + * 更新分组菜单名 + */ + private fun upGroupMenuName() { + val menuGroup = binding.toolBar.menu.findItem(R.id.menu_group) + val searchGroup = AppConfig.searchGroup + if (searchGroup.isEmpty()) { + menuGroup?.title = getString(R.string.group) + } else { + menuGroup?.title = getString(R.string.group) + "($searchGroup)" + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SOURCE_CHANGED) { + adapter.notifyItemRangeChanged( + 0, + adapter.itemCount, + bundleOf(Pair("upCurSource", oldBookUrl)) + ) + } + } + + interface CallBack { + val oldBook: Book? + fun changeTo(source: BookSource, book: Book, toc: List) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt new file mode 100755 index 000000000..ed2f54e1c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt @@ -0,0 +1,580 @@ +package io.legado.app.ui.book.changesource + +import android.app.Application +import android.os.Bundle +import androidx.annotation.CallSuper +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.SearchBook +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.primaryStr +import io.legado.app.help.book.releaseHtmlData +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.SourceConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.source.SourceHelp +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.internString +import io.legado.app.utils.mapParallel +import io.legado.app.utils.mapParallelSafe +import io.legado.app.utils.onEachIndexed +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import kotlin.math.min + +@Suppress("MemberVisibilityCanBePrivate") +open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(application) { + private val threadCount = AppConfig.threadCount + private var searchPool: ExecutorCoroutineDispatcher? = null + val searchStateData = MutableLiveData() + var searchFinishCallback: ((isEmpty: Boolean) -> Unit)? = null + var name: String = "" + var author: String = "" + private var fromReadBookActivity = false + private var oldBook: Book? = null + private var screenKey: String = "" + private var bookSourceParts = arrayListOf() + val totalSourceCount: Int + get() = bookSourceParts.size + private var searchBookList = arrayListOf() + private val searchBooks = Collections.synchronizedList(arrayListOf()) + private val tocMap = ConcurrentHashMap>() + private val _changeSourceProgress = MutableStateFlow(0 to "") + val changeSourceProgress = _changeSourceProgress.asStateFlow() + private var tocMapChapterCount = 0 + private val contentProcessor by lazy { + ContentProcessor.get(oldBook!!) + } + private var searchCallback: SourceCallback? = null + private val chapterNumRegex = "^\\[(\\d+)]".toRegex() + private val comparatorBase by lazy { + compareByDescending { getBookScore(it) } + .thenByDescending { SourceConfig.getSourceScore(it.origin) } + } + private val defaultComparator by lazy { + comparatorBase.thenBy { it.originOrder } + } + private val wordCountComparator by lazy { + comparatorBase.thenByDescending { it.chapterWordCount > 1000 } + .thenByDescending { getChapterNum(it.chapterWordCountText) } + .thenByDescending { it.chapterWordCount } + .thenBy { it.originOrder } + } + private var task: Job? = null + val bookMap = ConcurrentHashMap() + val searchDataFlow = callbackFlow { + + searchCallback = object : SourceCallback { + + override fun searchSuccess(searchBook: SearchBook) { + searchBook.releaseHtmlData() + appDb.searchBookDao.insert(searchBook) + when { + screenKey.isEmpty() -> searchBooks.add(searchBook) + searchBook.name.contains(screenKey) -> searchBooks.add(searchBook) + else -> return + } + trySend(arrayOf(searchBooks)) + } + + override fun upAdapter() { + trySend(arrayOf(searchBooks)) + } + + } + + getDbSearchBooks().let { + searchBooks.clear() + searchBooks.addAll(it) + trySend(arrayOf(searchBooks)) + } + + if (searchBooks.isEmpty()) { + startSearch() + } + + awaitClose { + searchCallback = null + } + }.map { + kotlin.runCatching { + val comparator = if (AppConfig.changeSourceLoadWordCount) { + wordCountComparator + } else { + defaultComparator + } + searchBooks.sortedWith(comparator) + }.onFailure { + AppLog.put("换源排序出错\n${it.localizedMessage}", it) + }.getOrDefault(searchBooks) + }.flowOn(IO) + + override fun onCleared() { + super.onCleared() + searchPool?.close() + } + + @CallSuper + open fun initData(arguments: Bundle?, book: Book?, fromReadBookActivity: Boolean) { + arguments?.let { bundle -> + bundle.getString("name")?.let { + name = it + } + bundle.getString("author")?.let { + author = it.replace(AppPattern.authorRegex, "") + } + this.fromReadBookActivity = fromReadBookActivity + oldBook = book + } + } + + private fun initSearchPool() { + searchPool = Executors + .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() + } + + fun refresh(): Boolean { + getDbSearchBooks().let { + searchBooks.clear() + searchBooks.addAll(it) + searchCallback?.upAdapter() + } + return searchBooks.isEmpty() + } + + /** + * 搜索书籍 + */ + fun startSearch() { + execute { + stopSearch() + if (searchBooks.isNotEmpty()) { + appDb.searchBookDao.delete(*searchBooks.toTypedArray()) + searchBooks.clear() + } + searchCallback?.upAdapter() + bookSourceParts.clear() + tocMap.clear() + bookMap.clear() + tocMapChapterCount = 0 + _changeSourceProgress.value = 0 to "" + val searchGroup = AppConfig.searchGroup + if (searchGroup.isBlank()) { + bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart) + } else { + val sources = appDb.bookSourceDao.getEnabledPartByGroup(searchGroup) + if (sources.isEmpty()) { + AppConfig.searchGroup = "" + bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart) + } else { + bookSourceParts.addAll(sources) + } + } + initSearchPool() + search() + } + } + + fun startSearch(origin: String) { + execute { + stopSearch() + bookSourceParts.clear() + tocMap.clear() + bookMap.clear() + tocMapChapterCount = 0 + bookSourceParts.add(appDb.bookSourceDao.getBookSourcePart(origin)!!) + searchBooks.removeIf { it.origin == origin } + initSearchPool() + search() + } + } + + private fun search() { + task = viewModelScope.launch(searchPool!!) { + flow { + for (bs in bookSourceParts) { + bs.getBookSource()?.let { + emit(it) + } + } + }.onStart { + searchStateData.postValue(true) + }.mapParallel(threadCount) { + try { + withTimeout(60000L) { + search(it) + } + } catch (_: Throwable) { + currentCoroutineContext().ensureActive() + } + it + }.onEachIndexed { index, value -> + _changeSourceProgress.update { _ -> + index + 1 to value.bookSourceName + } + }.onCompletion { + ensureActive() + searchStateData.postValue(false) + searchFinishCallback?.invoke(searchBooks.isEmpty()) + }.catch { + AppLog.put("换源搜索出错\n${it.localizedMessage}", it) + }.collect() + } + } + + private suspend fun search(source: BookSource) { + val checkAuthor = AppConfig.changeSourceCheckAuthor + val loadInfo = AppConfig.changeSourceLoadInfo + val loadToc = AppConfig.changeSourceLoadToc + val loadWordCount = AppConfig.changeSourceLoadWordCount + val resultBooks = WebBook.searchBookAwait( + source, name, + filter = { fName, fAuthor -> + fName == name && (!checkAuthor || fAuthor.contains(author)) + }) + resultBooks.forEach { searchBook -> + when { + loadInfo || loadToc || loadWordCount -> { + loadBookInfo(source, searchBook.toBook()) + } + + else -> { + searchCallback?.searchSuccess(searchBook) + } + } + } + } + + private suspend fun loadBookInfo(source: BookSource, book: Book) { + if (book.tocUrl.isEmpty()) { + WebBook.getBookInfoAwait(source, book) + } + if (AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount) { + loadBookToc(source, book) + } else { + //从详情页里获取最新章节 + val searchBook = book.toSearchBook() + searchCallback?.searchSuccess(searchBook) + } + } + + private suspend fun loadBookToc(source: BookSource, book: Book) { + val chapters = WebBook.getChapterListAwait(source, book).getOrThrow() + for (chapter in chapters) { + chapter.internString() + } + if (tocMapChapterCount < 30000) { + tocMapChapterCount += chapters.size + tocMap[book.primaryStr()] = chapters + } + bookMap[book.primaryStr()] = book + book.releaseHtmlData() + if (AppConfig.changeSourceLoadWordCount) { + loadBookWordCount(source, book, chapters) + } else { + val searchBook = book.toSearchBook() + searchCallback?.searchSuccess(searchBook) + } + } + + private suspend fun loadBookWordCount( + source: BookSource, + book: Book, + chapters: List + ) = coroutineScope { + val chapterIndex = if (fromReadBookActivity) { + BookHelp.getDurChapter(oldBook!!, chapters) + } else { + chapters.lastIndex + } + val bookChapter = chapters[chapterIndex] + var title = bookChapter.title.trim() + if (title.length > 20) { + title = title.substring(0, 20) + "…" + } + val startTime = System.currentTimeMillis() + val pair = try { + val nextChapterUrl = chapters.getOrNull(chapterIndex + 1)?.url + var content = WebBook.getContentAwait(source, book, bookChapter, nextChapterUrl, false) + content = contentProcessor.getContent(oldBook!!, bookChapter, content, false).toString() + val len = content.length + len to "[${chapterIndex + 1}] ${title}\n字数:${len}" + } catch (t: Throwable) { + if (t is CancellationException) throw t + -1 to "[${chapterIndex + 1}] ${title}\n获取字数失败:${t.localizedMessage}" + } + val endTime = System.currentTimeMillis() + val searchBook = book.toSearchBook().apply { + chapterWordCountText = pair.second + chapterWordCount = pair.first + respondTime = (endTime - startTime).toInt() + } + searchCallback?.searchSuccess(searchBook) + } + + fun onLoadWordCountChecked(isChecked: Boolean) { + if (isChecked) { + startRefreshList(true) + } + } + + /** + * 刷新列表 + */ + fun startRefreshList(onlyRefreshNoWordCountBook: Boolean = false) { + execute { + stopSearch() + searchBookList.clear() + if (onlyRefreshNoWordCountBook) { + searchBooks.filterTo(searchBookList) { + it.chapterWordCountText == null + } + searchBooks.removeIf { it.chapterWordCountText == null } + } else { + searchBookList.addAll(searchBooks) + searchBooks.clear() + } + searchCallback?.upAdapter() + initSearchPool() + refreshList() + } + } + + private fun refreshList() { + task = viewModelScope.launch(searchPool!!) { + flow { + for (searchBook in searchBookList) { + emit(searchBook) + } + }.onStart { + searchStateData.postValue(true) + }.mapParallelSafe(threadCount) { + val source = appDb.bookSourceDao.getBookSource(it.origin)!! + withTimeout(60000L) { + loadBookInfo(source, it.toBook()) + } + }.onCompletion { + searchStateData.postValue(false) + }.catch { + AppLog.put("换源刷新列表出错\n${it.localizedMessage}", it) + }.collect() + } + } + + private fun getDbSearchBooks(): List { + return if (screenKey.isEmpty()) { + if (AppConfig.changeSourceCheckAuthor) { + appDb.searchBookDao.changeSourceByGroup( + name, author, AppConfig.searchGroup + ) + } else { + appDb.searchBookDao.changeSourceByGroup( + name, "", AppConfig.searchGroup + ) + } + } else { + if (AppConfig.changeSourceCheckAuthor) { + appDb.searchBookDao.changeSourceSearch( + name, author, screenKey, AppConfig.searchGroup + ) + } else { + appDb.searchBookDao.changeSourceSearch( + name, "", screenKey, AppConfig.searchGroup + ) + } + } + } + + /** + * 筛选 + */ + fun screen(key: String?) { + screenKey = key?.trim() ?: "" + execute { + getDbSearchBooks().let { + searchBooks.clear() + searchBooks.addAll(it) + searchCallback?.upAdapter() + } + } + } + + fun startOrStopSearch() { + if (task == null || !task!!.isActive) { + startSearch() + } else { + stopSearch() + } + } + + fun stopSearch() { + task?.cancel() + searchPool?.close() + searchStateData.postValue(false) + } + + fun getToc( + book: Book, + onSuccess: (toc: List, source: BookSource) -> Unit, + onError: (e: Throwable) -> Unit + ): Coroutine, BookSource>> { + return execute { + val toc = tocMap[book.primaryStr()] + if (toc != null) { + val source = appDb.bookSourceDao.getBookSource(book.origin) + return@execute Pair(toc, source!!) + } + val result = getToc(book).getOrThrow() + tocMap[book.primaryStr()] = result.first + return@execute result + }.onSuccess { + onSuccess.invoke(it.first, it.second) + }.onError { + onError.invoke(it) + } + } + + suspend fun getToc(book: Book): Result, BookSource>> { + return kotlin.runCatching { + val source = appDb.bookSourceDao.getBookSource(book.origin) + ?: throw NoStackTraceException("书源不存在") + if (book.tocUrl.isEmpty()) { + WebBook.getBookInfoAwait(source, book) + } + val toc = WebBook.getChapterListAwait(source, book).getOrThrow() + Pair(toc, source) + } + } + + fun disableSource(searchBook: SearchBook) { + execute { + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> + source.enabled = false + appDb.bookSourceDao.update(source) + } + searchBooks.remove(searchBook) + searchCallback?.upAdapter() + } + } + + fun topSource(searchBook: SearchBook) { + execute { + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> + val minOrder = appDb.bookSourceDao.minOrder - 1 + source.customOrder = minOrder + searchBook.originOrder = source.customOrder + appDb.bookSourceDao.update(source) + updateSource(searchBook) + } + searchCallback?.upAdapter() + } + } + + fun bottomSource(searchBook: SearchBook) { + execute { + appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source -> + val maxOrder = appDb.bookSourceDao.maxOrder + 1 + source.customOrder = maxOrder + searchBook.originOrder = source.customOrder + appDb.bookSourceDao.update(source) + updateSource(searchBook) + } + searchCallback?.upAdapter() + } + } + + fun updateSource(searchBook: SearchBook) { + appDb.searchBookDao.update(searchBook) + } + + fun del(searchBook: SearchBook) { + execute { + SourceHelp.deleteBookSource(searchBook.origin) + appDb.searchBookDao.delete(searchBook) + } + searchBooks.remove(searchBook) + searchCallback?.upAdapter() + } + + fun autoChangeSource( + bookType: Int?, + onSuccess: (book: Book, toc: List, source: BookSource) -> Unit + ) { + execute { + searchBooks.forEach { + if (it.type == bookType) { + val book = it.toBook() + val result = getToc(book).getOrNull() + if (result != null) { + return@execute Triple(book, result.first, result.second) + } + } + } + throw NoStackTraceException("没有有效源") + }.onSuccess { + onSuccess.invoke(it.first, it.second, it.third) + }.onError { + context.toastOnUi("自动换源失败\n${it.localizedMessage}") + } + } + + fun setBookScore(searchBook: SearchBook, score: Int) { + execute { + SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score) + searchCallback?.upAdapter() + } + } + + fun getBookScore(searchBook: SearchBook): Int { + return SourceConfig.getBookScore(searchBook.origin, searchBook.name, searchBook.author) + } + + private fun getChapterNum(wordCountText: String?): Int { + wordCountText ?: return -1 + return chapterNumRegex.find(wordCountText)?.groupValues?.get(1)?.toIntOrNull() ?: -1 + } + + interface SourceCallback { + + fun searchSuccess(searchBook: SearchBook) + + fun upAdapter() + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt new file mode 100755 index 000000000..2eadeb03b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceAdapter.kt @@ -0,0 +1,205 @@ +package io.legado.app.ui.book.changesource + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemChangeSourceBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.visible +import splitties.init.appCtx +import splitties.views.onLongClick + + +class ChangeChapterSourceAdapter( + context: Context, + val viewModel: ChangeChapterSourceViewModel, + val callBack: CallBack +) : DiffRecyclerAdapter(context) { + + override val diffItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.bookUrl == newItem.bookUrl + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return oldItem.originName == newItem.originName + && oldItem.getDisplayLastChapterTitle() == newItem.getDisplayLastChapterTitle() + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemChangeSourceBinding { + return ItemChangeSourceBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemChangeSourceBinding, + item: SearchBook, + payloads: MutableList + ) { + binding.apply { + if (payloads.isEmpty()) { + tvOrigin.text = item.originName + tvAuthor.text = item.author + tvLast.text = item.getDisplayLastChapterTitle() + tvCurrentChapterWordCount.text = item.chapterWordCountText + tvRespondTime.text = context.getString(R.string.respondTime, item.respondTime) + if (callBack.oldBookUrl == item.bookUrl) { + ivChecked.visible() + } else { + ivChecked.invisible() + } + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "name" -> tvOrigin.text = item.originName + "latest" -> tvLast.text = item.getDisplayLastChapterTitle() + "upCurSource" -> if (callBack.oldBookUrl == item.bookUrl) { + ivChecked.visible() + } else { + ivChecked.invisible() + } + } + } + } + } + val score = callBack.getBookScore(item) + if (score > 0) { + binding.ivBad.gone() + binding.ivGood.visible() + DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200)) + DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100)) + } else if (score < 0) { + binding.ivGood.gone() + binding.ivBad.visible() + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_100) + ) + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_A200) + ) + } else { + binding.ivGood.visible() + binding.ivBad.visible() + DrawableCompat.setTint( + binding.ivGood.drawable, + appCtx.getCompatColor(R.color.md_red_100) + ) + DrawableCompat.setTint( + binding.ivBad.drawable, + appCtx.getCompatColor(R.color.md_blue_100) + ) + } + + if (AppConfig.changeSourceLoadWordCount && !item.chapterWordCountText.isNullOrBlank()) { + tvCurrentChapterWordCount.visible() + } else { + tvCurrentChapterWordCount.gone() + } + + if (AppConfig.changeSourceLoadWordCount && item.respondTime >= 0) { + tvRespondTime.visible() + } else { + tvRespondTime.gone() + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) { + binding.ivGood.setOnClickListener { + if (binding.ivBad.isVisible) { + DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200)) + binding.ivBad.gone() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, 1) + } + } else { + DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100)) + binding.ivBad.visible() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, 0) + } + } + } + binding.ivBad.setOnClickListener { + if (binding.ivGood.isVisible) { + DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200)) + binding.ivGood.gone() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, -1) + } + } else { + DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100)) + binding.ivGood.visible() + getItem(holder.layoutPosition)?.let { + callBack.setBookScore(it, 0) + } + } + } + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.openToc(it) + } + } + holder.itemView.onLongClick { + showMenu(holder.itemView, getItem(holder.layoutPosition)) + } + } + + private fun showMenu(view: View, searchBook: SearchBook?) { + searchBook ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.change_source_item) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_top_source -> { + callBack.topSource(searchBook) + } + R.id.menu_bottom_source -> { + callBack.bottomSource(searchBook) + } + R.id.menu_edit_source -> { + callBack.editSource(searchBook) + } + R.id.menu_disable_source -> { + callBack.disableSource(searchBook) + } + R.id.menu_delete_source -> { + callBack.deleteSource(searchBook) + updateItems(0, itemCount, listOf()) + } + } + true + } + popupMenu.show() + } + + interface CallBack { + val oldBookUrl: String? + fun openToc(searchBook: SearchBook) + fun topSource(searchBook: SearchBook) + fun bottomSource(searchBook: SearchBook) + fun editSource(searchBook: SearchBook) + fun disableSource(searchBook: SearchBook) + fun deleteSource(searchBook: SearchBook) + fun setBookScore(searchBook: SearchBook, score: Int) + fun getBookScore(searchBook: SearchBook): Int + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt new file mode 100755 index 000000000..5bb9b7a65 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt @@ -0,0 +1,414 @@ +package io.legado.app.ui.book.changesource + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.addCallback +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle.State.STARTED +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.DialogChapterChangeSourceBinding +import io.legado.app.help.book.BookHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.elevation +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx +import io.legado.app.utils.gone +import io.legado.app.utils.observeEvent +import io.legado.app.utils.setLayout +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + + +class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_change_source), + Toolbar.OnMenuItemClickListener, + ChangeChapterSourceAdapter.CallBack, + ChangeChapterTocAdapter.Callback { + + constructor(name: String, author: String, chapterIndex: Int, chapterTitle: String) : this() { + arguments = Bundle().apply { + putString("name", name) + putString("author", author) + putInt("chapterIndex", chapterIndex) + putString("chapterTitle", chapterTitle) + } + } + + private val binding by viewBinding(DialogChapterChangeSourceBinding::bind) + private val groups = linkedSetOf() + private val callBack: CallBack? get() = activity as? CallBack + private val viewModel: ChangeChapterSourceViewModel by viewModels() + private val editSourceResult = + registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) { + viewModel.startSearch() + } + private val searchBookAdapter by lazy { + ChangeChapterSourceAdapter(requireContext(), viewModel, this) + } + private val tocAdapter by lazy { + ChangeChapterTocAdapter(requireContext(), this) + } + private val contentSuccess: (content: String) -> Unit = { + binding.loadingToc.gone() + callBack?.replaceContent(it) + dismissAllowingStateLoss() + } + private var searchBook: SearchBook? = null + private val searchFinishCallback: (isEmpty: Boolean) -> Unit = { + if (it) { + val searchGroup = AppConfig.searchGroup + if (searchGroup.isNotEmpty()) { + lifecycleScope.launch { + context?.alert("搜索结果为空") { + setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组") + noButton() + yesButton { + AppConfig.searchGroup = "" + upGroupMenu() + viewModel.startSearch() + } + } + } + } + } + } + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + viewModel.initData(arguments, callBack?.oldBook, activity is ReadBookActivity) + showTitle() + initMenu() + initView() + initRecyclerView() + initSearchView() + initBottomBar() + initLiveData() + viewModel.searchFinishCallback = searchFinishCallback + activity?.onBackPressedDispatcher?.addCallback(this) { + if (binding.clToc.isVisible) { + binding.clToc.gone() + return@addCallback + } + dismissAllowingStateLoss() + } + } + + override fun onDestroy() { + super.onDestroy() + viewModel.searchFinishCallback = null + } + + private fun showTitle() { + binding.toolBar.title = viewModel.chapterTitle + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.change_source) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.menu.findItem(R.id.menu_check_author) + ?.isChecked = AppConfig.changeSourceCheckAuthor + binding.toolBar.menu.findItem(R.id.menu_load_info) + ?.isChecked = AppConfig.changeSourceLoadInfo + binding.toolBar.menu.findItem(R.id.menu_load_toc) + ?.isChecked = AppConfig.changeSourceLoadToc + binding.toolBar.menu.findItem(R.id.menu_load_word_count) + ?.isChecked = AppConfig.changeSourceLoadWordCount + } + + private fun initView() { + binding.ivHideToc.setOnClickListener { + binding.clToc.gone() + } + binding.flHideToc.elevation = requireContext().elevation + } + + private fun initRecyclerView() { + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = searchBookAdapter + searchBookAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0) { + binding.recyclerView.scrollToPosition(0) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + if (toPosition == 0) { + binding.recyclerView.scrollToPosition(0) + } + } + }) + binding.recyclerViewToc.adapter = tocAdapter + } + + private fun initSearchView() { + val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView + searchView.setOnCloseListener { + showTitle() + false + } + searchView.setOnSearchClickListener { + binding.toolBar.title = "" + binding.toolBar.subtitle = "" + } + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.screen(newText) + return false + } + + }) + } + + private fun initBottomBar() { + binding.tvDur.text = callBack?.oldBook?.originName + binding.tvDur.setOnClickListener { + scrollToDurSource() + } + binding.ivTop.setOnClickListener { + binding.recyclerView.scrollToPosition(0) + } + binding.ivBottom.setOnClickListener { + binding.recyclerView.scrollToPosition(searchBookAdapter.itemCount - 1) + } + } + + private fun initLiveData() { + viewModel.searchStateData.observe(viewLifecycleOwner) { + binding.refreshProgressBar.isAutoLoading = it + if (it) { + startStopMenuItem?.let { item -> + item.setIcon(R.drawable.ic_stop_black_24dp) + item.setTitle(R.string.stop) + } + } else { + startStopMenuItem?.let { item -> + item.setIcon(R.drawable.ic_refresh_black_24dp) + item.setTitle(R.string.refresh) + } + } + binding.toolBar.menu.applyTint(requireContext()) + } + lifecycleScope.launch { + lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) } + viewModel.searchDataFlow.conflate().collect { + searchBookAdapter.setItems(it) + delay(1000) + } + } + lifecycleScope.launch { + appDb.bookSourceDao.flowEnabledGroups().conflate().collect { + groups.clear() + groups.addAll(it) + upGroupMenu() + } + } + } + + private val startStopMenuItem: MenuItem? + get() = binding.toolBar.menu.findItem(R.id.menu_start_stop) + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_check_author -> { + AppConfig.changeSourceCheckAuthor = !item.isChecked + item.isChecked = !item.isChecked + viewModel.refresh() + } + + R.id.menu_load_info -> { + AppConfig.changeSourceLoadInfo = !item.isChecked + item.isChecked = !item.isChecked + } + + R.id.menu_load_toc -> { + AppConfig.changeSourceLoadToc = !item.isChecked + item.isChecked = !item.isChecked + } + + R.id.menu_load_word_count -> { + AppConfig.changeSourceLoadWordCount = !item.isChecked + item.isChecked = !item.isChecked + viewModel.onLoadWordCountChecked(item.isChecked) + } + + R.id.menu_start_stop -> viewModel.startOrStopSearch() + R.id.menu_source_manage -> startActivity() + else -> if (item?.groupId == R.id.source_group && !item.isChecked) { + item.isChecked = true + if (item.title.toString() == getString(R.string.all_source)) { + AppConfig.searchGroup = "" + } else { + AppConfig.searchGroup = item.title.toString() + } + lifecycleScope.launch(IO) { + viewModel.stopSearch() + if (viewModel.refresh()) { + viewModel.startSearch() + } + } + } + } + return false + } + + private fun scrollToDurSource() { + searchBookAdapter.getItems().forEachIndexed { index, searchBook -> + if (searchBook.bookUrl == oldBookUrl) { + (binding.recyclerView.layoutManager as LinearLayoutManager) + .scrollToPositionWithOffset(index, 60.dpToPx()) + return + } + } + } + + override fun openToc(searchBook: SearchBook) { + this.searchBook = searchBook + tocAdapter.setItems(null) + binding.clToc.visible() + binding.loadingToc.visible() + val book = searchBook.toBook() + viewModel.getToc(book, { toc: List, _: BookSource -> + tocAdapter.durChapterIndex = + BookHelp.getDurChapter(viewModel.chapterIndex, viewModel.chapterTitle, toc) + binding.loadingToc.gone() + tocAdapter.setItems(toc) + binding.recyclerViewToc.scrollToPosition(tocAdapter.durChapterIndex - 5) + }, { + binding.clToc.gone() + AppLog.put("单章换源获取目录出错\n$it", it, true) + }) + } + + override val oldBookUrl: String? + get() = callBack?.oldBook?.bookUrl + + override fun topSource(searchBook: SearchBook) { + viewModel.topSource(searchBook) + } + + override fun bottomSource(searchBook: SearchBook) { + viewModel.bottomSource(searchBook) + } + + override fun editSource(searchBook: SearchBook) { + editSourceResult.launch { + putExtra("sourceUrl", searchBook.origin) + } + } + + override fun disableSource(searchBook: SearchBook) { + viewModel.disableSource(searchBook) + } + + override fun deleteSource(searchBook: SearchBook) { + viewModel.del(searchBook) + if (oldBookUrl == searchBook.bookUrl) { + viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source -> + callBack?.changeTo(source, book, toc) + } + } + } + + override fun setBookScore(searchBook: SearchBook, score: Int) { + viewModel.setBookScore(searchBook, score) + } + + override fun getBookScore(searchBook: SearchBook): Int { + return viewModel.getBookScore(searchBook) + } + + override fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?) { + searchBook?.let { + binding.loadingToc.visible() + viewModel.getContent(it.toBook(), bookChapter, nextChapterUrl, contentSuccess) { msg -> + binding.loadingToc.gone() + binding.clToc.gone() + toastOnUi(msg) + } + } + } + + /** + * 更新分组菜单 + */ + private fun upGroupMenu() { + binding.toolBar.menu.findItem(R.id.menu_group)?.subMenu?.transaction { menu -> + val selectedGroup = AppConfig.searchGroup + menu.removeGroup(R.id.source_group) + val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) + var hasSelectedGroup = false + groups.forEach { group -> + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, group)?.let { + if (group == selectedGroup) { + it.isChecked = true + hasSelectedGroup = true + } + } + } + menu.setGroupCheckable(R.id.source_group, true, true) + if (!hasSelectedGroup) { + allItem.isChecked = true + } + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SOURCE_CHANGED) { + searchBookAdapter.notifyItemRangeChanged( + 0, + searchBookAdapter.itemCount, + bundleOf(Pair("upCurSource", oldBookUrl)) + ) + } + } + + interface CallBack { + val oldBook: Book? + fun changeTo(source: BookSource, book: Book, toc: List) + fun replaceContent(content: String) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt new file mode 100755 index 000000000..c97d0406d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt @@ -0,0 +1,46 @@ +package io.legado.app.ui.book.changesource + +import android.app.Application +import android.os.Bundle +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.webBook.WebBook + +@Suppress("MemberVisibilityCanBePrivate") +class ChangeChapterSourceViewModel(application: Application) : + ChangeBookSourceViewModel(application) { + + var chapterIndex: Int = 0 + var chapterTitle: String = "" + + override fun initData(arguments: Bundle?, book: Book?, fromReadBookActivity: Boolean) { + super.initData(arguments, book, fromReadBookActivity) + arguments?.let { bundle -> + bundle.getString("chapterTitle")?.let { + chapterTitle = it + } + chapterIndex = bundle.getInt("chapterIndex") + } + } + + fun getContent( + book: Book, + chapter: BookChapter, + nextChapterUrl: String?, + success: (content: String) -> Unit, + error: (msg: String) -> Unit + ) { + execute { + val bookSource = appDb.bookSourceDao.getBookSource(book.origin) + ?: throw NoStackTraceException("书源不存在") + WebBook.getContentAwait(bookSource, book, chapter, nextChapterUrl, false) + }.onSuccess { + success.invoke(it) + }.onError { + error.invoke(it.localizedMessage ?: "获取正文出错") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterTocAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterTocAdapter.kt new file mode 100755 index 000000000..722efbbeb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterTocAdapter.kt @@ -0,0 +1,70 @@ +package io.legado.app.ui.book.changesource + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.ItemChapterListBinding +import io.legado.app.lib.theme.ThemeUtils +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.visible + +class ChangeChapterTocAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + var durChapterIndex = 0 + + override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding { + return ItemChapterListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemChapterListBinding, + item: BookChapter, + payloads: MutableList + ) { + binding.run { + val isDur = durChapterIndex == item.index + if (isDur) { + tvChapterName.setTextColor(context.accentColor) + } else { + tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText)) + } + tvChapterName.text = item.title + if (item.isVolume) { + //卷名,如第一卷 突出显示 + tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press)) + } else { + //普通章节 保持不变 + tvChapterItem.background = + ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground) + } + if (!item.tag.isNullOrEmpty() && !item.isVolume) { + //卷名不显示tag(更新时间规则) + tvTag.text = item.tag + tvTag.visible() + } else { + tvTag.gone() + } + ivChecked.setImageResource(R.drawable.ic_check) + ivChecked.visible(isDur) + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callback.clickChapter(it, getItem(holder.layoutPosition + 1)?.url) + } + } + } + + interface Callback { + fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt new file mode 100755 index 000000000..bb660c3c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt @@ -0,0 +1,95 @@ +package io.legado.app.ui.book.explore + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ActivityExploreShowBinding +import io.legado.app.databinding.ViewLoadMoreBinding +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.widget.recycler.LoadMoreView +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 发现列表 + */ +class ExploreShowActivity : VMBaseActivity(), + ExploreShowAdapter.CallBack { + override val binding by viewBinding(ActivityExploreShowBinding::inflate) + override val viewModel by viewModels() + + private val adapter by lazy { ExploreShowAdapter(this, this) } + private val loadMoreView by lazy { LoadMoreView(this) } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.titleBar.title = intent.getStringExtra("exploreName") + initRecyclerView() + viewModel.booksData.observe(this) { upData(it) } + viewModel.initData(intent) + viewModel.errorLiveData.observe(this) { + loadMoreView.error(it) + } + viewModel.upAdapterLiveData.observe(this) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, bundleOf(it to null)) + } + } + + private fun initRecyclerView() { + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + adapter.addFooterView { + ViewLoadMoreBinding.bind(loadMoreView) + } + loadMoreView.startLoad() + loadMoreView.setOnClickListener { + if (!loadMoreView.isLoading) { + scrollToBottom(true) + } + } + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (!recyclerView.canScrollVertically(1)) { + scrollToBottom() + } + } + }) + } + + private fun scrollToBottom(forceLoad: Boolean = false) { + if ((loadMoreView.hasMore && !loadMoreView.isLoading) || forceLoad) { + loadMoreView.hasMore() + viewModel.explore() + } + } + + private fun upData(books: List) { + loadMoreView.stopLoad() + if (books.isEmpty() && adapter.isEmpty()) { + loadMoreView.noMore(getString(R.string.empty)) + } else if (adapter.getActualItemCount() == books.size) { + loadMoreView.noMore() + } else { + adapter.setItems(books) + } + } + + override fun isInBookshelf(book: SearchBook): Boolean { + return viewModel.isInBookShelf(book) + } + + override fun showBookInfo(book: SearchBook) { + startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + putExtra("bookUrl", book.bookUrl) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt new file mode 100755 index 000000000..84b1b6ae2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowAdapter.kt @@ -0,0 +1,97 @@ +package io.legado.app.ui.book.explore + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.core.view.isVisible +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemSearchBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class ExploreShowAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemSearchBinding { + return ItemSearchBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSearchBinding, + item: SearchBook, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + bind(binding, item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bindChange(binding, item, bundle) + } + } + + } + + private fun bind(binding: ItemSearchBinding, item: SearchBook) { + binding.run { + tvName.text = item.name + tvAuthor.text = context.getString(R.string.author_show, item.author) + ivInBookshelf.isVisible = callBack.isInBookshelf(item) + if (item.latestChapterTitle.isNullOrEmpty()) { + tvLasted.gone() + } else { + tvLasted.text = context.getString(R.string.lasted_show, item.latestChapterTitle) + tvLasted.visible() + } + tvIntroduce.text = item.trimIntro(context) + val kinds = item.getKindList() + if (kinds.isEmpty()) { + llKind.gone() + } else { + llKind.visible() + llKind.setLabels(kinds) + } + ivCover.load( + item.coverUrl, + item.name, + item.author, + AppConfig.loadCoverOnlyWifi, + item.origin + ) + } + } + + private fun bindChange(binding: ItemSearchBinding, item: SearchBook, bundle: Bundle) { + binding.run { + bundle.keySet().forEach { + when (it) { + "isInBookshelf" -> ivInBookshelf.isVisible = + callBack.isInBookshelf(item) + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSearchBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it) + } + } + } + + interface CallBack { + /** + * 是否已经加入书架 + */ + fun isInBookshelf(book: SearchBook): Boolean + + fun showBookInfo(book: SearchBook) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt new file mode 100755 index 000000000..59ca1783a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt @@ -0,0 +1,94 @@ +package io.legado.app.ui.book.explore + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.legado.app.BuildConfig +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.SearchBook +import io.legado.app.help.book.isNotShelf +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.stackTraceStr +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.mapLatest +import java.util.concurrent.ConcurrentHashMap + + +@OptIn(ExperimentalCoroutinesApi::class) +class ExploreShowViewModel(application: Application) : BaseViewModel(application) { + val bookshelf: MutableSet = ConcurrentHashMap.newKeySet() + val upAdapterLiveData = MutableLiveData() + val booksData = MutableLiveData>() + val errorLiveData = MutableLiveData() + private var bookSource: BookSource? = null + private var exploreUrl: String? = null + private var page = 1 + private var books = linkedSetOf() + + init { + execute { + appDb.bookDao.flowAll().mapLatest { books -> + val keys = arrayListOf() + books.filterNot { it.isNotShelf } + .forEach { + keys.add("${it.name}-${it.author}") + keys.add(it.name) + keys.add(it.bookUrl) + } + keys + }.catch { + AppLog.put("发现列表界面获取书籍数据失败\n${it.localizedMessage}", it) + }.collect { + bookshelf.clear() + bookshelf.addAll(it) + upAdapterLiveData.postValue("isInBookshelf") + } + }.onError { + AppLog.put("加载书架数据失败", it) + } + } + + fun initData(intent: Intent) { + execute { + val sourceUrl = intent.getStringExtra("sourceUrl") + exploreUrl = intent.getStringExtra("exploreUrl") + if (bookSource == null && sourceUrl != null) { + bookSource = appDb.bookSourceDao.getBookSource(sourceUrl) + } + explore() + } + } + + fun explore() { + val source = bookSource + val url = exploreUrl + if (source == null || url == null) return + WebBook.exploreBook(viewModelScope, source, url, page) + .timeout(if (BuildConfig.DEBUG) 0L else 30000L) + .onSuccess(IO) { searchBooks -> + books.addAll(searchBooks) + booksData.postValue(books.toList()) + appDb.searchBookDao.insert(*searchBooks.toTypedArray()) + page++ + }.onError { + it.printOnDebug() + errorLiveData.postValue(it.stackTraceStr) + } + } + + fun isInBookShelf(book: SearchBook): Boolean { + val name = book.name + val author = book.author + val bookUrl = book.bookUrl + val key = if (author.isNotBlank()) "$name-$author" else name + return bookshelf.contains(key) || bookshelf.contains(bookUrl) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt new file mode 100755 index 000000000..0235b747f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt @@ -0,0 +1,141 @@ +package io.legado.app.ui.book.group + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.DialogBookGroupEditBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.gone +import io.legado.app.utils.inputStream +import io.legado.app.utils.readUri +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import splitties.init.appCtx +import splitties.views.onClick +import java.io.FileOutputStream + +class GroupEditDialog() : BaseDialogFragment(R.layout.dialog_book_group_edit) { + + constructor(bookGroup: BookGroup? = null) : this() { + arguments = Bundle().apply { + putParcelable("group", bookGroup?.copy()) + } + } + + private val binding by viewBinding(DialogBookGroupEditBinding::bind) + private val viewModel by viewModels() + private var bookGroup: BookGroup? = null + private val selectImage = registerForActivityResult(HandleFileContract()) { + it.uri ?: return@registerForActivityResult + readUri(it.uri) { fileDoc, inputStream -> + try { + var file = requireContext().externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val fileName = it.uri.inputStream(requireContext()).getOrThrow().use { tmp -> + MD5Utils.md5Encode(tmp) + ".$suffix" + } + file = FileUtils.createFileIfNotExist(file, "covers", fileName) + FileOutputStream(file).use { outputStream -> + inputStream.copyTo(outputStream) + } + binding.ivCover.load(file.absolutePath) + } catch (e: Exception) { + appCtx.toastOnUi(e.localizedMessage) + } + } + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + @Suppress("DEPRECATION") + bookGroup = arguments?.getParcelable("group") + bookGroup?.let { + binding.btnDelete.visible(it.groupId > 0 || it.groupId == Long.MIN_VALUE) + binding.tieGroupName.setText(it.groupName) + binding.ivCover.load(it.cover) + if (it.bookSort + 1 !in 0.. Unit) { + alert(R.string.delete, R.string.sure_del) { + yesButton { + ok.invoke() + } + noButton() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt new file mode 100755 index 000000000..4152c8dc8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt @@ -0,0 +1,163 @@ +package io.legado.app.ui.book.group + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemBookGroupManageBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 书籍分组管理 + */ +class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val viewModel: GroupViewModel by viewModels() + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { GroupAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.group_manage) + initView() + initData() + initMenu() + } + + private fun initView() { + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + binding.tvOk.setTextColor(requireContext().accentColor) + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + dismissAllowingStateLoss() + } + } + + private fun initData() { + lifecycleScope.launch { + appDb.bookGroupDao.flowAll().catch { + AppLog.put("书籍分组管理界面获取分组数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { + adapter.setItems(it) + } + } + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.book_group_manage) + binding.toolBar.menu.applyTint(requireContext()) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> { + if (appDb.bookGroupDao.canAddGroup) { + showDialogFragment(GroupEditDialog()) + } else { + toastOnUi("分组已达上限(64个)") + } + } + } + return true + } + + private inner class GroupAdapter(context: Context) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private var isMoved = false + + override fun getViewBinding(parent: ViewGroup): ItemBookGroupManageBinding { + return ItemBookGroupManageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookGroupManageBinding, + item: BookGroup, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item.getManageName(context) + swShow.isChecked = item.show + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookGroupManageBinding) { + binding.run { + tvEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { bookGroup -> + showDialogFragment( + GroupEditDialog(bookGroup) + ) + } + } + swShow.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + viewModel.upGroup(it.copy(show = isChecked)) + } + } + } + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) + isMoved = true + return true + } + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.order = index + 1 + } + viewModel.upGroup(*getItems().toTypedArray()) + } + isMoved = false + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt new file mode 100755 index 000000000..7c196b1e0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt @@ -0,0 +1,167 @@ +package io.legado.app.ui.book.group + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.DialogBookGroupPickerBinding +import io.legado.app.databinding.ItemGroupSelectBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch + + +class GroupSelectDialog() : BaseDialogFragment(R.layout.dialog_book_group_picker), + Toolbar.OnMenuItemClickListener { + + constructor(groupId: Long, requestCode: Int = -1) : this() { + arguments = Bundle().apply { + putLong("groupId", groupId) + putInt("requestCode", requestCode) + } + } + + private val binding by viewBinding(DialogBookGroupPickerBinding::bind) + private var requestCode: Int = -1 + private val viewModel: GroupViewModel by viewModels() + private val adapter by lazy { GroupAdapter(requireContext()) } + private val callBack get() = (activity as? CallBack) + private var groupId: Long = 0 + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + arguments?.let { + groupId = it.getLong("groupId") + requestCode = it.getInt("requestCode", -1) + } + initView() + initData() + } + + private fun initView() { + binding.toolBar.title = getString(R.string.group_select) + binding.toolBar.inflateMenu(R.menu.book_group_manage) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.setTextColor(requireContext().accentColor) + binding.tvOk.setOnClickListener { + callBack?.upGroup(requestCode, groupId) + dismissAllowingStateLoss() + } + } + + private fun initData() { + lifecycleScope.launch { + appDb.bookGroupDao.flowSelect().conflate().collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> showDialogFragment( + GroupEditDialog() + ) + } + return true + } + + private inner class GroupAdapter(context: Context) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private var isMoved: Boolean = false + + override fun getViewBinding(parent: ViewGroup): ItemGroupSelectBinding { + return ItemGroupSelectBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupSelectBinding, + item: BookGroup, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + cbGroup.text = item.groupName + cbGroup.isChecked = (groupId and item.groupId) > 0 + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupSelectBinding) { + binding.run { + cbGroup.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + groupId = if (isChecked) { + groupId + it.groupId + } else { + groupId - it.groupId + } + } + } + tvEdit.setOnClickListener { + showDialogFragment( + GroupEditDialog(getItem(holder.layoutPosition)) + ) + } + } + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) + isMoved = true + return true + } + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.order = index + 1 + } + viewModel.upGroup(*getItems().toTypedArray()) + } + isMoved = false + } + } + + interface CallBack { + fun upGroup(requestCode: Int, groupId: Long) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt new file mode 100755 index 000000000..ea535aa72 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.book.group + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookGroup + +class GroupViewModel(application: Application) : BaseViewModel(application) { + + fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) { + execute { + appDb.bookGroupDao.update(*bookGroup) + }.onFinally { + finally?.invoke() + } + } + + fun addGroup( + groupName: String, + bookSort: Int, + enableRefresh: Boolean, + cover: String?, + finally: () -> Unit + ) { + execute { + val groupId = appDb.bookGroupDao.getUnusedId() + val bookGroup = BookGroup( + groupId = groupId, + groupName = groupName, + cover = cover, + bookSort = bookSort, + enableRefresh = enableRefresh, + order = appDb.bookGroupDao.maxOrder.plus(1) + ) + appDb.bookGroupDao.getByID(groupId) ?: appDb.bookDao.removeGroup(groupId) + appDb.bookGroupDao.insert(bookGroup) + }.onFinally { + finally() + } + } + + fun delGroup(bookGroup: BookGroup, finally: () -> Unit) { + execute { + appDb.bookGroupDao.delete(bookGroup) + appDb.bookDao.removeGroup(bookGroup.groupId) + }.onFinally { + finally() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/BaseImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/import/BaseImportBookActivity.kt new file mode 100755 index 000000000..1385499bc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/BaseImportBookActivity.kt @@ -0,0 +1,161 @@ +package io.legado.app.ui.book.import + +import android.os.Bundle +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.ViewModel +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppPattern +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ActivityImportBookBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.model.localBook.LocalBook +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.applyTint +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +abstract class BaseImportBookActivity : + VMBaseActivity() { + + final override val binding by viewBinding(ActivityImportBookBinding::inflate) + + private var localBookTreeSelectListener: ((Boolean) -> Unit)? = null + protected val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + + val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { + it.uri?.let { treeUri -> + AppConfig.defaultBookTreeUri = treeUri.toString() + localBookTreeSelectListener?.invoke(true) + } ?: localBookTreeSelectListener?.invoke(false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initSearchView() + } + + /** + * 设置书籍保存位置 + */ + protected suspend fun setBookStorage() = suspendCancellableCoroutine sc@{ block -> + localBookTreeSelectListener = { + localBookTreeSelectListener = null + block.resume(it) + } + //测试书籍保存位置是否设置 + if (!AppConfig.defaultBookTreeUri.isNullOrBlank()) { + localBookTreeSelectListener = null + block.resume(true) + return@sc + } + //测试读写?? + val storageHelp = String(assets.open("storageHelp.md").readBytes()) + val hint = getString(R.string.select_book_folder) + alert(hint, storageHelp) { + okButton { + localBookTreeSelect.launch { + title = hint + } + } + cancelButton { + localBookTreeSelectListener = null + block.resume(false) + } + onCancelled { + localBookTreeSelectListener = null + block.resume(false) + } + } + } + + abstract fun onSearchTextChange(newText: String?) + + protected fun startReadBook(book: Book) { + startActivityForBook(book) + } + + protected fun onArchiveFileClick(fileDoc: FileDoc) { + val fileNames = ArchiveUtils.getArchiveFilesName(fileDoc) { + it.matches(AppPattern.bookFileRegex) + } + if (fileNames.size == 1) { + val name = fileNames[0] + appDb.bookDao.getBookByFileName(name)?.let { + startReadBook(it) + } ?: showImportAlert(fileDoc, name) + } else { + showSelectBookReadAlert(fileDoc, fileNames) + } + } + + private fun showSelectBookReadAlert(fileDoc: FileDoc, fileNames: List) { + if (fileNames.isEmpty()) { + toastOnUi(R.string.unsupport_archivefile_entry) + return + } + selector( + R.string.start_read, + fileNames + ) { _, name, _ -> + appDb.bookDao.getBookByFileName(name)?.let { + startReadBook(it) + } ?: showImportAlert(fileDoc, name) + } + } + + /* 添加压缩包内指定文件到书架 */ + private inline fun addArchiveToBookShelf( + fileDoc: FileDoc, + fileName: String, + onSuccess: (Book) -> Unit + ) { + LocalBook.importArchiveFile(fileDoc.uri, fileName) { + it.contains(fileName) + }.firstOrNull()?.run { + onSuccess.invoke(this) + } + } + + /* 提示是否重新导入所点击的压缩文件 */ + private fun showImportAlert(fileDoc: FileDoc, fileName: String) { + alert( + R.string.draw, + R.string.no_book_found_bookshelf + ) { + okButton { + addArchiveToBookShelf(fileDoc, fileName) { + startReadBook(it) + } + } + noButton() + } + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + onSearchTextChange(newText) + return false + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBook.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBook.kt new file mode 100755 index 000000000..3fa74d5ab --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBook.kt @@ -0,0 +1,14 @@ +package io.legado.app.ui.book.import.local + +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.FileDoc + +data class ImportBook( + val file: FileDoc, + var isOnBookShelf: Boolean = !file.isDir && LocalBook.isOnBookShelf(file.name) +) { + val name get() = file.name + val isDir get() = file.isDir + val size get() = file.size + val lastModified get() = file.lastModified +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt new file mode 100755 index 000000000..6fb6cf529 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt @@ -0,0 +1,327 @@ +package io.legado.app.ui.book.import.local + +import android.annotation.SuppressLint +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.book.import.BaseImportBookActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.gone +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.isUri +import io.legado.app.utils.launch +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + +/** + * 导入本地书籍界面 + */ +class ImportBookActivity : BaseImportBookActivity(), + PopupMenu.OnMenuItemClickListener, + ImportBookAdapter.CallBack, + SelectActionBar.CallBack { + + override val viewModel by viewModels() + private val adapter by lazy { ImportBookAdapter(this, this) } + private var scanDocJob: Job? = null + + private val selectFolder = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + AppConfig.importBookPath = uri.toString() + initRootDoc(true) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + searchView.queryHint = getString(R.string.screen) + " • " + getString(R.string.local_book) + onBackPressedDispatcher.addCallback(this) { + if (!goBackDir()) { + finish() + } + } + lifecycleScope.launch { + initView() + initEvent() + if (setBookStorage() && AppConfig.importBookPath.isNullOrBlank()) { + AppConfig.importBookPath = AppConfig.defaultBookTreeUri + } + initData() + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.import_book, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_sort_name)?.isChecked = viewModel.sort == 0 + menu.findItem(R.id.menu_sort_size)?.isChecked = viewModel.sort == 1 + menu.findItem(R.id.menu_sort_time)?.isChecked = viewModel.sort == 2 + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_select_folder -> selectFolder.launch() + R.id.menu_scan_folder -> scanFolder() + R.id.menu_import_file_name -> alertImportFileName() + R.id.menu_sort_name -> upSort(0) + R.id.menu_sort_size -> upSort(1) + R.id.menu_sort_time -> upSort(2) + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_del_selection -> viewModel.deleteDoc(adapter.selected) { + adapter.removeSelection() + } + } + return false + } + + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + @SuppressLint("NotifyDataSetChanged") + override fun onClickSelectBarMainAction() { + viewModel.addToBookshelf(adapter.selected) { + adapter.selected.forEach { + it.isOnBookShelf = true + } + adapter.selected.clear() + adapter.notifyDataSetChanged() + } + } + + private fun initView() { + binding.layTop.setBackgroundColor(backgroundColor) + binding.tvEmptyMsg.setText(R.string.empty_msg_import_book) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.recyclerView.recycledViewPool.setMaxRecycledViews(0, 15) + binding.selectActionBar.setMainActionText(R.string.add_to_bookshelf) + binding.selectActionBar.inflateMenu(R.menu.import_book_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun initEvent() { + binding.tvGoBack.setOnClickListener { + goBackDir() + } + } + + private fun initData() { + viewModel.dataFlowStart = { + initRootDoc() + } + lifecycleScope.launch { + viewModel.dataFlow.conflate().collect { docs -> + adapter.setItems(docs) + } + } + } + + private fun initRootDoc(changedFolder: Boolean = false) { + if (viewModel.rootDoc != null && !changedFolder) { + upPath() + } else { + val lastPath = AppConfig.importBookPath + if (lastPath.isNullOrBlank()) { + binding.tvEmptyMsg.visible() + selectFolder.launch() + } else { + val rootUri = if (lastPath.isUri()) { + lastPath.toUri() + } else { + Uri.fromFile(File(lastPath)) + } + when { + rootUri.isContentScheme() -> initRootPath(rootUri) + else -> initRootPath(rootUri.path!!) + } + } + } + } + + private fun initRootPath(rootUri: Uri) { + kotlin.runCatching { + val doc = DocumentFile.fromTreeUri(this, rootUri) + if (doc == null || doc.name.isNullOrEmpty() || !doc.isDirectory) { + binding.tvEmptyMsg.visible() + selectFolder.launch() + } else { + viewModel.subDocs.clear() + viewModel.rootDoc = FileDoc.fromDocumentFile(doc) + upPath() + } + }.onFailure { + binding.tvEmptyMsg.visible() + selectFolder.launch() + } + } + + private fun initRootPath(path: String) { + binding.tvEmptyMsg.visible() + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + kotlin.runCatching { + val file = File(path) + if (!file.isDirectory) { + binding.tvEmptyMsg.visible() + selectFolder.launch() + } else { + viewModel.subDocs.clear() + viewModel.rootDoc = FileDoc.fromFile(file) + upPath() + } + }.onFailure { + binding.tvEmptyMsg.visible() + selectFolder.launch() + } + } + .request() + } + + private fun upSort(sort: Int) { + viewModel.sort = sort + putPrefInt(PreferKey.localBookImportSort, sort) + if (scanDocJob?.isActive != true) { + viewModel.dataCallback?.upAdapter() + } + } + + @Synchronized + private fun upPath() { + binding.tvGoBack.isEnabled = viewModel.subDocs.isNotEmpty() + viewModel.rootDoc?.let { + scanDocJob?.cancel() + upDocs(it) + } + } + + private fun upDocs(rootDoc: FileDoc) { + binding.tvEmptyMsg.gone() + var path = rootDoc.name + File.separator + var lastDoc = rootDoc + for (doc in viewModel.subDocs) { + lastDoc = doc + path = path + doc.name + File.separator + } + binding.tvPath.text = path + adapter.selected.clear() + adapter.clearItems() + viewModel.loadDoc(lastDoc) + } + + /** + * 扫描当前文件夹及所有子文件夹 + */ + private fun scanFolder() { + viewModel.rootDoc?.let { doc -> + adapter.clearItems() + val lastDoc = viewModel.subDocs.lastOrNull() ?: doc + binding.refreshProgressBar.isAutoLoading = true + scanDocJob?.cancel() + scanDocJob = lifecycleScope.launch(IO) { + viewModel.scanDoc(lastDoc) + withContext(Main) { + binding.refreshProgressBar.isAutoLoading = false + } + } + } + } + + private fun alertImportFileName() { + alert(R.string.import_file_name) { + setMessage("""使用js处理文件名变量src,将书名作者分别赋值到变量name author""") + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "js" + editView.setText(AppConfig.bookImportFileName) + } + customView { alertBinding.root } + okButton { + AppConfig.bookImportFileName = alertBinding.editView.text?.toString() + } + cancelButton() + } + } + + @Synchronized + override fun nextDoc(fileDoc: FileDoc) { + viewModel.subDocs.add(fileDoc) + upPath() + } + + @Synchronized + private fun goBackDir(): Boolean { + return if (viewModel.subDocs.isNotEmpty()) { + viewModel.subDocs.removeAt(viewModel.subDocs.lastIndex) + upPath() + true + } else { + false + } + } + + override fun onSearchTextChange(newText: String?) { + viewModel.updateCallBackFlow(newText) + } + + override fun upCountView() { + binding.selectActionBar.upCountView(adapter.selected.size, adapter.checkableCount) + } + + override fun startRead(fileDoc: FileDoc) { + if (!ArchiveUtils.isArchive(fileDoc.name)) { + appDb.bookDao.getBookByFileName(fileDoc.name)?.let { + val filePath = fileDoc.toString() + if (it.bookUrl != filePath) { + it.bookUrl = filePath + appDb.bookDao.insert(it) + } + startReadBook(it) + } + } else { + onArchiveFileClick(fileDoc) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookAdapter.kt new file mode 100755 index 000000000..050c294ae --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookAdapter.kt @@ -0,0 +1,141 @@ +package io.legado.app.ui.book.import.local + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppConst +import io.legado.app.databinding.ItemImportBookBinding +import io.legado.app.utils.ConvertUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.visible + + +class ImportBookAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + val selected = hashSetOf() + var checkableCount = 0 + + override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding { + return ItemImportBookBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { + upCheckableCount() + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemImportBookBinding, + item: ImportBook, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + if (item.isDir) { + ivIcon.setImageResource(R.drawable.ic_folder) + ivIcon.visible() + cbSelect.invisible() + llBrief.gone() + cbSelect.isChecked = false + } else { + if (item.isOnBookShelf) { + ivIcon.setImageResource(R.drawable.ic_book_has) + ivIcon.visible() + cbSelect.invisible() + } else { + ivIcon.invisible() + cbSelect.visible() + } + llBrief.visible() + tvTag.text = item.name.substringAfterLast(".") + tvSize.text = ConvertUtils.formatFileSize(item.size) + tvDate.text = AppConst.dateFormat.format(item.lastModified) + cbSelect.isChecked = selected.contains(item) + } + tvName.text = item.name + } else { + cbSelect.isChecked = selected.contains(item) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemImportBookBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (it.isDir) { + callBack.nextDoc(it.file) + } else if (!it.isOnBookShelf) { + if (!selected.contains(it)) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(holder.layoutPosition, true) + callBack.upCountView() + } else { + /* 点击开始阅读 */ + callBack.startRead(it.file) + } + } + } + } + + private fun upCheckableCount() { + checkableCount = 0 + getItems().forEach { + if (!it.isDir && !it.isOnBookShelf) { + checkableCount++ + } + } + callBack.upCountView() + } + + @SuppressLint("NotifyDataSetChanged") + fun selectAll(selectAll: Boolean) { + if (selectAll) { + getItems().forEach { + if (!it.isDir && !it.isOnBookShelf) { + selected.add(it) + } + } + } else { + selected.clear() + } + notifyDataSetChanged() + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (!it.isDir && !it.isOnBookShelf) { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + } + notifyItemRangeChanged(0, itemCount, true) + callBack.upCountView() + } + + fun removeSelection() { + for (i in getItems().lastIndex downTo 0) { + if (getItem(i) in selected) { + removeItem(i) + } + } + } + + interface CallBack { + fun nextDoc(fileDoc: FileDoc) + fun upCountView() + fun startRead(fileDoc: FileDoc) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt new file mode 100755 index 000000000..2490ba91d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt @@ -0,0 +1,181 @@ +package io.legado.app.ui.book.import.local + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern.archiveFileRegex +import io.legado.app.constant.AppPattern.bookFileRegex +import io.legado.app.constant.PreferKey +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.AlphanumComparator +import io.legado.app.utils.FileDoc +import io.legado.app.utils.delete +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.list +import io.legado.app.utils.mapParallel +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.withContext +import java.util.Collections + +class ImportBookViewModel(application: Application) : BaseViewModel(application) { + var rootDoc: FileDoc? = null + val subDocs = arrayListOf() + var sort = context.getPrefInt(PreferKey.localBookImportSort) + var dataCallback: DataCallback? = null + var dataFlowStart: (() -> Unit)? = null + var filterKey: String? = null + val dataFlow = callbackFlow> { + + val list = Collections.synchronizedList(ArrayList()) + + dataCallback = object : DataCallback { + + override fun setItems(fileDocs: List) { + list.clear() + fileDocs.mapTo(list) { + ImportBook(it) + } + trySend(list) + } + + override fun addItems(fileDocs: List) { + fileDocs.mapTo(list) { + ImportBook(it) + } + trySend(list) + } + + override fun clear() { + list.clear() + trySend(emptyList()) + } + + override fun upAdapter() { + trySend(list) + } + } + + withContext(Main) { + dataFlowStart?.invoke() + } + + awaitClose { + dataCallback = null + } + + }.map { docList -> + val docList = docList.toList() + val filterKey = filterKey + val skipFilter = filterKey.isNullOrBlank() + val comparator = when (sort) { + 2 -> compareBy({ !it.isDir }, { -it.lastModified }) + 1 -> compareBy({ !it.isDir }, { -it.size }) + else -> compareBy { !it.isDir } + } then compareBy(AlphanumComparator) { it.name } + docList.asSequence().filter { + skipFilter || it.name.contains(filterKey) + }.sortedWith(comparator).toList() + }.flowOn(IO) + + fun addToBookshelf(bookList: HashSet, finally: () -> Unit) { + execute { + val fileUris = bookList.map { + it.file.uri + } + LocalBook.importFiles(fileUris) + }.onError { + context.toastOnUi("添加书架失败,请尝试重新选择文件夹") + AppLog.put("添加书架失败\n${it.localizedMessage}", it) + }.onSuccess { + context.toastOnUi("添加书架成功") + }.onFinally { + finally.invoke() + } + } + + fun deleteDoc(bookList: HashSet, finally: () -> Unit) { + execute { + bookList.forEach { + it.file.delete() + } + }.onFinally { + finally.invoke() + } + } + + fun loadDoc(fileDoc: FileDoc) { + execute { + val docList = fileDoc.list { item -> + when { + item.name.startsWith(".") -> false + item.isDir -> true + else -> item.name.matches(bookFileRegex) || item.name.matches(archiveFileRegex) + } + } + dataCallback?.setItems(docList!!) + }.onError { + context.toastOnUi("获取文件列表出错\n${it.localizedMessage}") + } + } + + suspend fun scanDoc(fileDoc: FileDoc) { + dataCallback?.clear() + val channel = Channel(UNLIMITED) + var n = 1 + channel.trySend(fileDoc) + val list = arrayListOf() + channel.consumeAsFlow() + .mapParallel(16) { fileDoc -> + fileDoc.list()!! + }.onEach { fileDocs -> + n-- + list.clear() + fileDocs.forEach { + if (it.isDir) { + n++ + channel.trySend(it) + } else if (it.name.matches(bookFileRegex) + || it.name.matches(archiveFileRegex) + ) { + list.add(it) + } + } + dataCallback?.addItems(list) + }.takeWhile { + n > 0 + }.catch { + context.toastOnUi("扫描文件夹出错\n${it.localizedMessage}") + }.collect() + } + + fun updateCallBackFlow(filterKey: String?) { + this.filterKey = filterKey + dataCallback?.upAdapter() + } + + interface DataCallback { + + fun setItems(fileDocs: List) + + fun addItems(fileDocs: List) + + fun clear() + + fun upAdapter() + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookActivity.kt new file mode 100755 index 000000000..5c30c8cd3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookActivity.kt @@ -0,0 +1,259 @@ +package io.legado.app.ui.book.import.remote + +import android.annotation.SuppressLint +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.core.view.isGone +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.model.remote.RemoteBook +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.import.BaseImportBookActivity +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.find +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch +import java.io.File + +/** + * 展示远程书籍 + */ +class RemoteBookActivity : BaseImportBookActivity(), + RemoteBookAdapter.CallBack, + SelectActionBar.CallBack, + ServersDialog.Callback { + + override val viewModel by viewModels() + private val adapter by lazy { RemoteBookAdapter(this, this) } + private var groupMenu: SubMenu? = null + + override fun onActivityCreated(savedInstanceState: Bundle?) { + searchView.queryHint = getString(R.string.screen) + " • " + getString(R.string.remote_book) + onBackPressedDispatcher.addCallback(this) { + if (!goBackDir()) { + finish() + } + } + lifecycleScope.launch { + if (!setBookStorage()) { + finish() + return@launch + } + initView() + initEvent() + launch { + viewModel.dataFlow.conflate().collect { sortedRemoteBooks -> + binding.refreshProgressBar.isAutoLoading = false + binding.tvEmptyMsg.isGone = sortedRemoteBooks.isNotEmpty() + adapter.setItems(sortedRemoteBooks) + delay(500) + } + } + viewModel.initData { + upPath() + } + } + } + + override fun observeLiveBus() { + viewModel.permissionDenialLiveData.observe(this) { + localBookTreeSelect.launch { + title = getString(R.string.select_book_folder) + } + } + } + + private fun initView() { + binding.layTop.setBackgroundColor(backgroundColor) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.selectActionBar.setMainActionText(R.string.add_to_bookshelf) + binding.selectActionBar.setCallBack(this) + if (!LocalConfig.webDavBookHelpVersionIsLast) { + showHelp("webDavBookHelp") + } + } + + private fun sortCheck(sortKey: RemoteBookSort) { + if (viewModel.sortKey == sortKey) { + viewModel.sortAscending = !viewModel.sortAscending + } else { + viewModel.sortAscending = true + viewModel.sortKey = sortKey + } + } + + private fun initEvent() { + binding.tvGoBack.setOnClickListener { + goBackDir() + } + } + + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_remote, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_refresh -> upPath() + R.id.menu_server_config -> showDialogFragment() + R.id.menu_log -> showDialogFragment() + R.id.menu_help -> showHelp("webDavBookHelp") + R.id.menu_sort_name -> { + item.isChecked = true + sortCheck(RemoteBookSort.Name) + upPath() + } + R.id.menu_sort_time -> { + item.isChecked = true + sortCheck(RemoteBookSort.Default) + upPath() + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + groupMenu = menu.findItem(R.id.menu_sort)?.subMenu + groupMenu?.setGroupCheckable(R.id.menu_group_sort, true, true) + groupMenu?.findItem(R.id.menu_sort_name)?.isChecked = + viewModel.sortKey == RemoteBookSort.Name + groupMenu?.findItem(R.id.menu_sort_time)?.isChecked = + viewModel.sortKey == RemoteBookSort.Default + return super.onPrepareOptionsMenu(menu) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + @SuppressLint("NotifyDataSetChanged") + override fun onClickSelectBarMainAction() { + binding.refreshProgressBar.isAutoLoading = true + viewModel.addToBookshelf(adapter.selected) { + adapter.selected.clear() + adapter.notifyDataSetChanged() + binding.refreshProgressBar.isAutoLoading = false + } + } + + private fun goBackDir(): Boolean { + if (viewModel.dirList.isEmpty()) { + return false + } + viewModel.dirList.removeLastOrNull() + upPath() + return true + } + + private fun upPath() { + binding.tvGoBack.isEnabled = viewModel.dirList.isNotEmpty() + var path = if (viewModel.isDefaultWebdav) { + "books" + File.separator + } else { + File.separator + } + viewModel.dirList.forEach { + path = path + it.filename + File.separator + } + binding.tvPath.text = path + viewModel.dataCallback?.clear() + adapter.selected.clear() + viewModel.loadRemoteBookList( + viewModel.dirList.lastOrNull()?.path + ) { + binding.refreshProgressBar.isAutoLoading = it + } + } + + override fun openDir(remoteBook: RemoteBook) { + viewModel.dirList.add(remoteBook) + upPath() + } + + override fun upCountView() { + binding.selectActionBar.upCountView(adapter.selected.size, adapter.checkableCount) + } + + override fun onDialogDismiss(tag: String) { + viewModel.initData { + upPath() + } + } + + override fun onSearchTextChange(newText: String?) { + viewModel.updateCallBackFlow(newText) + } + + private fun showRemoteBookDownloadAlert( + remoteBook: RemoteBook, + onDownloadFinish: (() -> Unit)? = null + ) { + alert( + R.string.draw, + R.string.archive_not_found + ) { + okButton { + viewModel.addToBookshelf(hashSetOf(remoteBook)) { + onDownloadFinish?.invoke() + } + } + noButton() + } + } + + override fun startRead(remoteBook: RemoteBook) { + val downloadFileName = remoteBook.filename + if (!ArchiveUtils.isArchive(downloadFileName)) { + appDb.bookDao.getBookByFileName(downloadFileName)?.let { + startReadBook(it) + } + } else { + AppConfig.defaultBookTreeUri ?: return + val downloadArchiveFileDoc = FileDoc.fromUri(Uri.parse(AppConfig.defaultBookTreeUri), true) + .find(downloadFileName) + if (downloadArchiveFileDoc == null) { + showRemoteBookDownloadAlert(remoteBook) { + startRead(remoteBook) + } + } else { + onArchiveFileClick(downloadArchiveFileDoc) + } + } + } + + override fun addToBookShelfAgain(remoteBook: RemoteBook) { + alert(getString(R.string.sure), "是否重新加入书架?") { + yesButton { + binding.refreshProgressBar.isAutoLoading = true + viewModel.addToBookshelf(hashSetOf(remoteBook)) { + binding.refreshProgressBar.isAutoLoading = false + } + } + noButton() + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookAdapter.kt new file mode 100755 index 000000000..64ce19c65 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookAdapter.kt @@ -0,0 +1,157 @@ +package io.legado.app.ui.book.import.remote + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppConst +import io.legado.app.databinding.ItemImportBookBinding +import io.legado.app.model.remote.RemoteBook +import io.legado.app.utils.ConvertUtils +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.visible + + +/** + * 适配器 + * @author qianfanguojin + */ +class RemoteBookAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + var selected = hashSetOf() + var checkableCount = 0 + + override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding { + return ItemImportBookBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { + upCheckableCount() + } + + /** + * 绑定RecycleView 中每一个项的视图和数据 + */ + override fun convert( + holder: ItemViewHolder, + binding: ItemImportBookBinding, + item: RemoteBook, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + if (item.isDir) { + ivIcon.setImageResource(R.drawable.ic_folder) + ivIcon.visible() + cbSelect.invisible() + llBrief.gone() + cbSelect.isChecked = false + } else { + if (item.isOnBookShelf) { + ivIcon.setImageResource(R.drawable.ic_book_has) + ivIcon.visible() + cbSelect.invisible() + } else { + ivIcon.invisible() + cbSelect.visible() + } + llBrief.visible() + tvTag.text = item.contentType + tvSize.text = ConvertUtils.formatFileSize(item.size) + tvDate.text = AppConst.dateFormat.format(item.lastModify) + cbSelect.isChecked = selected.contains(item) + } + tvName.text = item.filename + } else { + cbSelect.isChecked = selected.contains(item) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemImportBookBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (it.isDir) { + callBack.openDir(it) + } else if (!it.isOnBookShelf) { + if (!selected.contains(it)) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(holder.layoutPosition, true) + callBack.upCountView() + } else { + /* 点击开始阅读 */ + callBack.startRead(it) + } + } + } + holder.itemView.setOnLongClickListener { + getItem(holder.layoutPosition)?.let { remoteBook -> + if (remoteBook.isOnBookShelf) { + callBack.addToBookShelfAgain(remoteBook) + } + } + true + } + } + + private fun upCheckableCount() { + checkableCount = 0 + getItems().forEach { + if (!it.isDir && !it.isOnBookShelf) { + checkableCount++ + } + } + callBack.upCountView() + } + + @SuppressLint("NotifyDataSetChanged") + fun selectAll(selectAll: Boolean) { + if (selectAll) { + getItems().forEach { + if (!it.isDir && !it.isOnBookShelf) { + selected.add(it) + } + } + } else { + selected.clear() + } + notifyDataSetChanged() + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (!it.isDir && !it.isOnBookShelf) { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + } + notifyItemRangeChanged(0, itemCount, true) + callBack.upCountView() + } + + fun removeSelection() { + for (i in getItems().lastIndex downTo 0) { + if (getItem(i) in selected) { + removeItem(i) + } + } + } + + + interface CallBack { + fun openDir(remoteBook: RemoteBook) + fun upCountView() + fun startRead(remoteBook: RemoteBook) + fun addToBookShelfAgain(remoteBook: RemoteBook) + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookSort.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookSort.kt new file mode 100755 index 000000000..41964035e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookSort.kt @@ -0,0 +1,5 @@ +package io.legado.app.ui.book.import.remote + +enum class RemoteBookSort { + Default, Name +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt new file mode 100755 index 000000000..51be1e278 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt @@ -0,0 +1,174 @@ +package io.legado.app.ui.book.import.remote + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.data.appDb +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.webdav.Authorization +import io.legado.app.model.analyzeRule.CustomUrl +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.remote.RemoteBook +import io.legado.app.model.remote.RemoteBookWebDav +import io.legado.app.utils.AlphanumComparator +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import java.util.Collections + +class RemoteBookViewModel(application: Application) : BaseViewModel(application) { + var sortKey = RemoteBookSort.Default + var sortAscending = false + val dirList = arrayListOf() + val permissionDenialLiveData = MutableLiveData() + + var dataCallback: DataCallback? = null + + val dataFlow = callbackFlow> { + + val list = Collections.synchronizedList(ArrayList()) + + dataCallback = object : DataCallback { + + override fun setItems(remoteFiles: List) { + list.clear() + list.addAll(remoteFiles) + trySend(list) + } + + override fun addItems(remoteFiles: List) { + list.addAll(remoteFiles) + trySend(list) + } + + override fun clear() { + list.clear() + trySend(emptyList()) + } + + override fun screen(key: String?) { + if (key.isNullOrBlank()) { + trySend(list) + } else { + trySend( + list.filter { it.filename.contains(key) } + ) + } + } + } + + awaitClose { + dataCallback = null + } + }.map { list -> + if (sortAscending) when (sortKey) { + RemoteBookSort.Name -> list.sortedWith(compareBy { !it.isDir } + then compareBy(AlphanumComparator) { it.filename }) + + else -> list.sortedWith(compareBy({ !it.isDir }, { it.lastModify })) + } else when (sortKey) { + RemoteBookSort.Name -> list.sortedWith { o1, o2 -> + val compare = -compareValues(o1.isDir, o2.isDir) + if (compare == 0) { + return@sortedWith -AlphanumComparator.compare(o1.filename, o2.filename) + } + return@sortedWith compare + } + + else -> list.sortedWith { o1, o2 -> + val compare = -compareValues(o1.isDir, o2.isDir) + if (compare == 0) { + return@sortedWith -compareValues(o1.lastModify, o2.lastModify) + } + return@sortedWith compare + } + } + }.flowOn(Dispatchers.IO) + + private var remoteBookWebDav: RemoteBookWebDav? = null + var isDefaultWebdav = false + + fun initData(onSuccess: () -> Unit) { + execute { + isDefaultWebdav = false + appDb.serverDao.get(AppConfig.remoteServerId)?.getWebDavConfig()?.let { + val authorization = Authorization(it) + remoteBookWebDav = RemoteBookWebDav(it.url, authorization, AppConfig.remoteServerId) + return@execute + } + isDefaultWebdav = true + remoteBookWebDav = AppWebDav.defaultBookWebDav + ?: throw NoStackTraceException("webDav没有配置") + }.onError { + context.toastOnUi("初始化webDav出错:${it.localizedMessage}") + }.onSuccess { + onSuccess.invoke() + } + } + + fun loadRemoteBookList(path: String?, loadCallback: (loading: Boolean) -> Unit) { + executeLazy { + val bookWebDav = remoteBookWebDav + ?: throw NoStackTraceException("没有配置webDav") + dataCallback?.clear() + val url = path ?: bookWebDav.rootBookUrl + val bookList = bookWebDav.getRemoteBookList(url) + dataCallback?.setItems(bookList) + }.onError { + AppLog.put("获取webDav书籍出错\n${it.localizedMessage}", it) + context.toastOnUi("获取webDav书籍出错\n${it.localizedMessage}") + }.onStart { + loadCallback.invoke(true) + }.onFinally { + loadCallback.invoke(false) + }.start() + } + + fun addToBookshelf(remoteBooks: HashSet, finally: () -> Unit) { + execute { + val bookWebDav = remoteBookWebDav + ?: throw NoStackTraceException("没有配置webDav") + remoteBooks.forEach { remoteBook -> + val downloadBookUri = bookWebDav.downloadRemoteBook(remoteBook) + LocalBook.importFiles(downloadBookUri).forEach { book -> + book.origin = BookType.webDavTag + CustomUrl(remoteBook.path) + .putAttribute("serverID", bookWebDav.serverID) + .toString() + book.save() + } + remoteBook.isOnBookShelf = true + } + }.onError { + AppLog.put("导入出错\n${it.localizedMessage}", it) + context.toastOnUi("导入出错\n${it.localizedMessage}") + if (it is SecurityException) { + permissionDenialLiveData.postValue(1) + } + }.onFinally { + finally.invoke() + } + } + + fun updateCallBackFlow(filterKey: String?) { + dataCallback?.screen(filterKey) + } + + interface DataCallback { + + fun setItems(remoteFiles: List) + + fun addItems(remoteFiles: List) + + fun clear() + + fun screen(key: String?) + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt new file mode 100755 index 000000000..ba4f91f47 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt @@ -0,0 +1,131 @@ +package io.legado.app.ui.book.import.remote + +import android.os.Bundle +import android.text.InputType +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.entities.Server +import io.legado.app.data.entities.rule.RowUi +import io.legado.app.databinding.DialogWebdavServerBinding +import io.legado.app.databinding.ItemSourceEditBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.GSON +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import org.json.JSONObject + +class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, true), + Toolbar.OnMenuItemClickListener { + + constructor(id: Long) : this() { + arguments = Bundle().apply { + putLong("id", id) + } + } + + private val binding by viewBinding(DialogWebdavServerBinding::bind) + private val viewModel by viewModels() + + private val webDavServerUi = listOf( + RowUi("url"), + RowUi("username"), + RowUi("password", RowUi.Type.password) + ) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.inflateMenu(R.menu.server_config) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + viewModel.init(arguments?.getLong("id")) { + upConfigView(viewModel.mServer) + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> getServer().let { + viewModel.save(it) { + dismissAllowingStateLoss() + } + } + } + return true + } + + private fun upConfigView(server: Server?) { + binding.etName.setText(server?.name) + binding.spType.setSelection( + when (server?.type) { + else -> 0 + } + ) + when (server?.type) { + else -> upWebDavServerUi(server?.getConfigJsonObject()) + } + } + + private fun upWebDavServerUi(config: JSONObject?) { + webDavServerUi.forEachIndexed { index, rowUi -> + when (rowUi.type) { + RowUi.Type.text -> ItemSourceEditBinding.inflate( + layoutInflater, + binding.root, + false + ).let { + binding.flexbox.addView(it.root) + it.root.id = index + 1000 + it.textInputLayout.hint = rowUi.name + it.editText.setText(config?.getString(rowUi.name)) + } + RowUi.Type.password -> ItemSourceEditBinding.inflate( + layoutInflater, + binding.root, + false + ).let { + binding.flexbox.addView(it.root) + it.root.id = index + 1000 + it.textInputLayout.hint = rowUi.name + it.editText.inputType = + InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT + it.editText.setText(config?.getString(rowUi.name)) + } + } + } + } + + private fun getServer(): Server { + val server = viewModel.mServer?.copy() ?: Server() + server.name = binding.etName.text.toString() + server.type = when (binding.spType.selectedItemPosition) { + else -> Server.TYPE.WEBDAV + } + server.config = when (server.type) { + else -> GSON.toJson(getWebDavConfig()) + } + return server + } + + private fun getWebDavConfig(): HashMap { + val data = hashMapOf() + webDavServerUi.forEachIndexed { index, rowUi -> + val rowView = binding.root.findViewById(index + 1000) + ItemSourceEditBinding.bind(rowView).editText.text?.let { + data[rowUi.name] = it.toString() + } + } + return data + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt new file mode 100755 index 000000000..875304232 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt @@ -0,0 +1,41 @@ +package io.legado.app.ui.book.import.remote + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Server +import io.legado.app.utils.toastOnUi + +class ServerConfigViewModel(application: Application): BaseViewModel(application) { + + var mServer: Server? = null + + fun init(id: Long?, onSuccess: () -> Unit) { + //mServer不为空可能是旋转屏幕界面重新创建,不用更新数据 + if (mServer != null) return + execute { + mServer = if (id != null) { + appDb.serverDao.get(id) + } else { + Server() + } + }.onSuccess { + onSuccess.invoke() + } + } + + fun save(server: Server, onSuccess: () -> Unit) { + execute { + mServer?.let { + appDb.serverDao.delete(it) + } + mServer = server + appDb.serverDao.insert(server) + }.onSuccess { + onSuccess.invoke() + }.onError { + context.toastOnUi("保存出错\n${it.localizedMessage}") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt new file mode 100755 index 000000000..3bf9990b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt @@ -0,0 +1,167 @@ +package io.legado.app.ui.book.import.remote + +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppConst.DEFAULT_WEBDAV_ID +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Server +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemServerSelectBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 服务器配置 + */ +class ServersDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + val binding by viewBinding(DialogRecyclerViewBinding::bind) + val viewModel by viewModels() + + private val callback get() = (activity as? Callback) + private val adapter by lazy { ServersAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.server_config) + initView() + initData() + } + + private fun initView() { + binding.toolBar.inflateMenu(R.menu.servers) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + binding.tvFooterLeft.text = getString(R.string.text_default) + binding.tvFooterLeft.visible() + binding.tvFooterLeft.setOnClickListener { + AppConfig.remoteServerId = DEFAULT_WEBDAV_ID + dismissAllowingStateLoss() + } + binding.tvCancel.visible() + binding.tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvOk.visible() + binding.tvOk.setOnClickListener { + AppConfig.remoteServerId = adapter.selectServerId + dismissAllowingStateLoss() + } + } + + private fun initData() { + lifecycleScope.launch { + appDb.serverDao.observeAll().catch { + AppLog.put("服务器配置界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add -> showDialogFragment(ServerConfigDialog()) + } + return true + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + callback?.onDialogDismiss("serversDialog") + } + + inner class ServersAdapter(context: Context) : + RecyclerAdapter(context) { + + var selectServerId: Long = AppConfig.remoteServerId + + override fun getViewBinding(parent: ViewGroup): ItemServerSelectBinding { + return ItemServerSelectBinding.inflate(inflater, parent, false) + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemServerSelectBinding) { + binding.rbServer.setOnUserCheckedChangeListener { isChecked -> + if (isChecked) { + selectServerId = getItemByLayoutPosition(holder.layoutPosition)!!.id + adapter.updateItems(0, itemCount - 1, "upSelect") + } + } + binding.ivEdit.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { server -> + showDialogFragment(ServerConfigDialog(server.id)) + } + } + binding.ivDelete.setOnClickListener { + alert { + setTitle(R.string.draw) + setMessage(R.string.sure_del) + yesButton { + getItemByLayoutPosition(holder.layoutPosition)?.let { server -> + viewModel.delete(server) + } + } + noButton() + } + } + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemServerSelectBinding, + item: Server, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + binding.root.setBackgroundColor(context.backgroundColor) + binding.rbServer.text = item.name + binding.rbServer.isChecked = item.id == selectServerId + } else { + binding.rbServer.isChecked = item.id == selectServerId + } + } + + } + + interface Callback { + + fun onDialogDismiss(tag: String) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt new file mode 100755 index 000000000..0508e87cb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt @@ -0,0 +1,17 @@ +package io.legado.app.ui.book.import.remote + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Server + +class ServersViewModel(application: Application): BaseViewModel(application) { + + + fun delete(server: Server) { + execute { + appDb.serverDao.delete(server) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt new file mode 100755 index 000000000..2012a8f03 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -0,0 +1,771 @@ +package io.legado.app.ui.book.info + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.widget.CheckBox +import android.widget.LinearLayout +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.BookType +import io.legado.app.constant.Theme +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.ActivityBookInfoBinding +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.addType +import io.legado.app.help.book.getRemoteUrl +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isImage +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isLocalTxt +import io.legado.app.help.book.isWebFile +import io.legado.app.help.book.removeType +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.model.BookCover +import io.legado.app.model.remote.RemoteBookWebDav +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.ui.book.changecover.ChangeCoverDialog +import io.legado.app.ui.book.changesource.ChangeBookSourceDialog +import io.legado.app.ui.book.group.GroupSelectDialog +import io.legado.app.ui.book.info.edit.BookInfoEditActivity +import io.legado.app.ui.book.manga.ReadMangaActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.widget.dialog.PhotoDialog +import io.legado.app.ui.widget.dialog.VariableDialog +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.ConvertUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.GSON +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.dpToPx +import io.legado.app.utils.gone +import io.legado.app.utils.longToastOnUi +import io.legado.app.utils.openFileUri +import io.legado.app.utils.sendToClip +import io.legado.app.utils.shareWithQr +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class BookInfoActivity : + VMBaseActivity(toolBarTheme = Theme.Dark), + GroupSelectDialog.CallBack, + ChangeBookSourceDialog.CallBack, + ChangeCoverDialog.CallBack, + VariableDialog.Callback { + + private val tocActivityResult = registerForActivityResult(TocActivityResult()) { + it?.let { + viewModel.getBook(false)?.let { book -> + lifecycleScope.launch { + withContext(IO) { + book.durChapterIndex = it.first + book.durChapterPos = it.second + chapterChanged = it.third + appDb.bookDao.update(book) + } + startReadActivity(book) + } + } + } ?: let { + if (!viewModel.inBookshelf) { + viewModel.delBook() + } + } + } + private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { + it.uri?.let { treeUri -> + AppConfig.defaultBookTreeUri = treeUri.toString() + } + } + private val readBookResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + viewModel.upBook(intent) + when (it.resultCode) { + RESULT_OK -> { + viewModel.inBookshelf = true + upTvBookshelf() + } + + RESULT_DELETED -> { + setResult(RESULT_OK) + finish() + } + } + } + private val infoEditResult = registerForActivityResult( + StartActivityContract(BookInfoEditActivity::class.java) + ) { + if (it.resultCode == RESULT_OK) { + viewModel.upEditBook() + } + } + private val editSourceResult = registerForActivityResult( + StartActivityContract(BookSourceEditActivity::class.java) + ) { + if (it.resultCode == RESULT_CANCELED) { + return@registerForActivityResult + } + book?.let { book -> + viewModel.bookSource = appDb.bookSourceDao.getBookSource(book.origin) + viewModel.refreshBook(book) + } + } + private var chapterChanged = false + private val waitDialog by lazy { WaitDialog(this) } + private var editMenuItem: MenuItem? = null + private val book get() = viewModel.getBook(false) + + override val binding by viewBinding(ActivityBookInfoBinding::inflate) + override val viewModel by viewModels() + + @SuppressLint("PrivateResource") + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.titleBar.setBackgroundResource(R.color.transparent) + binding.refreshLayout?.setColorSchemeColors(accentColor) + binding.arcView.setBgColor(backgroundColor) + binding.llInfo.setBackgroundColor(backgroundColor) + binding.flAction.setBackgroundColor(bottomBackground) + binding.flAction.applyNavigationBarPadding() + binding.tvShelf.setTextColor(getPrimaryTextColor(ColorUtils.isColorLight(bottomBackground))) + binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + binding.tvIntro.revealOnFocusHint = false + } + viewModel.bookData.observe(this) { showBook(it) } + viewModel.chapterListData.observe(this) { upLoading(false, it) } + viewModel.waitDialogData.observe(this) { upWaitDialogStatus(it) } + viewModel.initData(intent) + initViewEvent() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_info, menu) + editMenuItem = menu.findItem(R.id.menu_edit) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_can_update)?.isChecked = + viewModel.bookData.value?.canUpdate ?: true + menu.findItem(R.id.menu_split_long_chapter)?.isChecked = + viewModel.bookData.value?.getSplitLongChapter() ?: true + menu.findItem(R.id.menu_login)?.isVisible = + !viewModel.bookSource?.loginUrl.isNullOrBlank() + menu.findItem(R.id.menu_set_source_variable)?.isVisible = + viewModel.bookSource != null + menu.findItem(R.id.menu_set_book_variable)?.isVisible = + viewModel.bookSource != null + menu.findItem(R.id.menu_can_update)?.isVisible = + viewModel.bookSource != null + menu.findItem(R.id.menu_split_long_chapter)?.isVisible = + viewModel.bookData.value?.isLocalTxt ?: false + menu.findItem(R.id.menu_upload)?.isVisible = + viewModel.bookData.value?.isLocal ?: false + menu.findItem(R.id.menu_delete_alert)?.isChecked = + LocalConfig.bookInfoDeleteAlert + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_edit -> { + viewModel.getBook()?.let { + infoEditResult.launch { + putExtra("bookUrl", it.bookUrl) + } + } + } + + R.id.menu_share_it -> { + viewModel.getBook()?.let { + val bookJson = GSON.toJson(it) + val shareStr = "${it.bookUrl}#$bookJson" + shareWithQr(shareStr, it.name) + } + } + + R.id.menu_refresh -> { + refreshBook() + } + + R.id.menu_login -> viewModel.bookSource?.let { + startActivity { + putExtra("type", "bookSource") + putExtra("key", it.bookSourceUrl) + } + } + + R.id.menu_top -> viewModel.topBook() + R.id.menu_set_source_variable -> setSourceVariable() + R.id.menu_set_book_variable -> setBookVariable() + R.id.menu_copy_book_url -> viewModel.getBook()?.bookUrl?.let { + sendToClip(it) + } + + R.id.menu_copy_toc_url -> viewModel.getBook()?.tocUrl?.let { + sendToClip(it) + } + + R.id.menu_can_update -> { + viewModel.getBook()?.let { + it.canUpdate = !it.canUpdate + if (viewModel.inBookshelf) { + if (!it.canUpdate) { + it.removeType(BookType.updateError) + } + viewModel.saveBook(it) + } + } + } + + R.id.menu_clear_cache -> viewModel.clearCache() + R.id.menu_log -> showDialogFragment() + R.id.menu_split_long_chapter -> { + upLoading(true) + viewModel.getBook()?.let { + it.setSplitLongChapter(!item.isChecked) + viewModel.loadBookInfo(it, false) + } + item.isChecked = !item.isChecked + if (!item.isChecked) longToastOnUi(R.string.need_more_time_load_content) + } + + R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked + R.id.menu_upload -> { + viewModel.getBook()?.let { book -> + book.getRemoteUrl()?.let { + alert(R.string.draw, R.string.sure_upload) { + okButton { + upLoadBook(book) + } + cancelButton() + } + } ?: upLoadBook(book) + } + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun observeLiveBus() { + viewModel.actionLive.observe(this) { + when (it) { + "selectBooksDir" -> localBookTreeSelect.launch { + title = getString(R.string.select_book_folder) + } + } + } + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (ev.action == MotionEvent.ACTION_DOWN) { + currentFocus?.let { + if (it === binding.tvIntro && binding.tvIntro.hasSelection()) { + it.clearFocus() + } + } + } + return super.dispatchTouchEvent(ev) + } + + private fun refreshBook() { + upLoading(true) + viewModel.getBook()?.let { + viewModel.refreshBook(it) + } + } + + private fun upLoadBook( + book: Book, + bookWebDav: RemoteBookWebDav? = AppWebDav.defaultBookWebDav, + ) { + lifecycleScope.launch { + waitDialog.setText("上传中.....") + waitDialog.show() + try { + bookWebDav + ?.upload(book) + ?: throw NoStackTraceException("未配置webDav") + //更新书籍最后更新时间,使之比远程书籍的时间新 + book.lastCheckTime = System.currentTimeMillis() + viewModel.saveBook(book) + } catch (e: Exception) { + toastOnUi(e.localizedMessage) + } finally { + waitDialog.dismiss() + } + } + } + + private fun showBook(book: Book) = binding.run { + showCover(book) + tvName.text = book.name + tvAuthor.text = getString(R.string.author_show, book.getRealAuthor()) + tvOrigin.text = getString(R.string.origin_show, book.originName) + tvLasted.text = getString(R.string.lasted_show, book.latestChapterTitle) + tvIntro.text = book.getDisplayIntro() + llToc?.visible(!book.isWebFile) + upTvBookshelf() + upKinds(book) + upGroup(book.group) + } + + private fun upKinds(book: Book) = binding.run { + lifecycleScope.launch { + var kinds = book.getKindList() + if (book.isLocal) { + withContext(IO) { + val size = FileDoc.fromFile(book.bookUrl).size + if (size > 0) { + kinds = kinds.toMutableList() + kinds.add(ConvertUtils.formatFileSize(size)) + } + } + } + if (kinds.isEmpty()) { + lbKind.gone() + } else { + lbKind.visible() + lbKind.setLabels(kinds) + } + } + } + + private fun showCover(book: Book) { + binding.ivCover.load(book.getDisplayCover(), book.name, book.author, false, book.origin) { + if (!AppConfig.isEInkMode) { + BookCover.loadBlur(this, book.getDisplayCover(), false, book.origin) + .into(binding.bgBook) + } + } + } + + private fun upLoading(isLoading: Boolean, chapterList: List? = null) { + when { + isLoading -> { + binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading)) + } + + chapterList.isNullOrEmpty() -> { + binding.tvToc.text = getString( + R.string.toc_s, + getString(R.string.error_load_toc) + ) + } + + else -> { + book?.let { + binding.tvToc.text = getString(R.string.toc_s, it.durChapterTitle) + binding.tvLasted.text = getString(R.string.lasted_show, it.latestChapterTitle) + } + } + } + } + + private fun upTvBookshelf() { + if (viewModel.inBookshelf) { + binding.tvShelf.text = getString(R.string.remove_from_bookshelf) + } else { + binding.tvShelf.text = getString(R.string.add_to_bookshelf) + } + editMenuItem?.isVisible = viewModel.inBookshelf + } + + private fun upGroup(groupId: Long) { + viewModel.loadGroup(groupId) { + if (it.isNullOrEmpty()) { + binding.tvGroup.text = if (book?.isLocal == true) { + getString(R.string.group_s, getString(R.string.local_no_group)) + } else { + getString(R.string.group_s, getString(R.string.no_group)) + } + } else { + binding.tvGroup.text = getString(R.string.group_s, it) + } + } + } + + private fun initViewEvent() = binding.run { + ivCover.setOnClickListener { + viewModel.getBook()?.let { + showDialogFragment( + ChangeCoverDialog(it.name, it.author) + ) + } + } + ivCover.setOnLongClickListener { + viewModel.getBook()?.getDisplayCover()?.let { path -> + showDialogFragment(PhotoDialog(path)) + } + true + } + tvRead.setOnClickListener { + viewModel.getBook()?.let { book -> + if (book.isWebFile) { + showWebFileDownloadAlert { + readBook(it) + } + } else { + readBook(book) + } + } + } + tvShelf.setOnClickListener { + viewModel.getBook()?.let { book -> + if (viewModel.inBookshelf) { + deleteBook() + } else { + if (book.isWebFile) { + showWebFileDownloadAlert() + } else { + viewModel.addToBookshelf { + upTvBookshelf() + } + } + } + } + } + tvOrigin.setOnClickListener { + viewModel.getBook()?.let { book -> + if (book.isLocal) return@let + if (!appDb.bookSourceDao.has(book.origin)) { + toastOnUi(R.string.error_no_source) + return@let + } + editSourceResult.launch { + putExtra("sourceUrl", book.origin) + } + } + } + tvChangeSource.setOnClickListener { + viewModel.getBook()?.let { book -> + showDialogFragment(ChangeBookSourceDialog(book.name, book.author)) + } + } + tvTocView.setOnClickListener { + if (viewModel.chapterListData.value.isNullOrEmpty()) { + toastOnUi(R.string.chapter_list_empty) + return@setOnClickListener + } + viewModel.getBook()?.let { book -> + if (!viewModel.inBookshelf) { + viewModel.saveBook(book) { + viewModel.saveChapterList { + openChapterList() + } + } + } else { + openChapterList() + } + } + } + tvChangeGroup.setOnClickListener { + viewModel.getBook()?.let { + showDialogFragment( + GroupSelectDialog(it.group) + ) + } + } + tvAuthor.setOnClickListener { + viewModel.getBook(false)?.let { book -> + startActivity { + putExtra("key", book.author) + } + } + } + tvName.setOnClickListener { + viewModel.getBook(false)?.let { book -> + startActivity { + putExtra("key", book.name) + } + } + } + refreshLayout?.setOnRefreshListener { + refreshLayout.isRefreshing = false + refreshBook() + } + } + + private fun setSourceVariable() { + lifecycleScope.launch { + val source = viewModel.bookSource + if (source == null) { + toastOnUi("书源不存在") + return@launch + } + val comment = + source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取") + val variable = withContext(IO) { source.getVariable() } + showDialogFragment( + VariableDialog( + getString(R.string.set_source_variable), + source.getKey(), + variable, + comment + ) + ) + } + } + + private fun setBookVariable() { + lifecycleScope.launch { + val source = viewModel.bookSource + if (source == null) { + toastOnUi("书源不存在") + return@launch + } + val book = viewModel.getBook() ?: return@launch + val variable = withContext(IO) { book.getCustomVariable() } + val comment = source.getDisplayVariableComment( + """书籍变量可在js中通过book.getVariable("custom")获取""" + ) + showDialogFragment( + VariableDialog( + getString(R.string.set_book_variable), + book.bookUrl, + variable, + comment + ) + ) + } + } + + override fun setVariable(key: String, variable: String?) { + when (key) { + viewModel.bookSource?.getKey() -> viewModel.bookSource?.setVariable(variable) + viewModel.bookData.value?.bookUrl -> viewModel.bookData.value?.let { + it.putCustomVariable(variable) + if (viewModel.inBookshelf) { + viewModel.saveBook(it) + } + } + } + } + + @SuppressLint("InflateParams") + private fun deleteBook() { + viewModel.getBook()?.let { + if (LocalConfig.bookInfoDeleteAlert) { + alert( + titleResource = R.string.draw, + messageResource = R.string.sure_del + ) { + var checkBox: CheckBox? = null + if (it.isLocal) { + checkBox = CheckBox(this@BookInfoActivity).apply { + setText(R.string.delete_book_file) + isChecked = LocalConfig.deleteBookOriginal + } + val view = LinearLayout(this@BookInfoActivity).apply { + setPadding(16.dpToPx(), 0, 16.dpToPx(), 0) + addView(checkBox) + } + customView { view } + } + yesButton { + if (checkBox != null) { + LocalConfig.deleteBookOriginal = checkBox.isChecked + } + viewModel.delBook(LocalConfig.deleteBookOriginal) { + setResult(RESULT_OK) + finish() + } + } + noButton() + } + } else { + viewModel.delBook(LocalConfig.deleteBookOriginal) { + setResult(RESULT_OK) + finish() + } + } + } + } + + private fun openChapterList() { + viewModel.getBook()?.let { + tocActivityResult.launch(it.bookUrl) + } + } + + private fun showWebFileDownloadAlert( + onClick: ((Book) -> Unit)? = null, + ) { + val webFiles = viewModel.webFiles + if (webFiles.isEmpty()) { + toastOnUi("Unexpected webFileData") + return + } + selector( + R.string.download_and_import_file, + webFiles + ) { _, webFile, _ -> + if (webFile.isSupported) { + /* import */ + viewModel.importOrDownloadWebFile(webFile) { + onClick?.invoke(it) + } + } else if (webFile.isSupportDecompress) { + /* 解压筛选后再选择导入项 */ + viewModel.importOrDownloadWebFile(webFile) { uri -> + viewModel.getArchiveFilesName(uri) { fileNames -> + if (fileNames.size == 1) { + viewModel.importArchiveBook(uri, fileNames[0]) { + onClick?.invoke(it) + } + } else { + showDecompressFileImportAlert(uri, fileNames, onClick) + } + } + } + } else { + alert( + title = getString(R.string.draw), + message = getString(R.string.file_not_supported, webFile.name) + ) { + neutralButton(R.string.open_fun) { + /* download only */ + viewModel.importOrDownloadWebFile(webFile) { + openFileUri(it, "*/*") + } + } + noButton() + } + } + } + } + + private fun showDecompressFileImportAlert( + archiveFileUri: Uri, + fileNames: List, + success: ((Book) -> Unit)? = null, + ) { + if (fileNames.isEmpty()) { + toastOnUi(R.string.unsupport_archivefile_entry) + return + } + selector( + R.string.import_select_book, + fileNames + ) { _, name, _ -> + viewModel.importArchiveBook(archiveFileUri, name) { + success?.invoke(it) + } + } + } + + private fun readBook(book: Book) { + if (!viewModel.inBookshelf) { + book.addType(BookType.notShelf) + viewModel.saveBook(book) { + viewModel.saveChapterList { + startReadActivity(book) + } + } + } else { + viewModel.saveBook(book) { + startReadActivity(book) + } + } + } + + private fun startReadActivity(book: Book) { + when { + book.isAudio -> readBookResult.launch( + Intent(this, AudioPlayActivity::class.java) + .putExtra("bookUrl", book.bookUrl) + .putExtra("inBookshelf", viewModel.inBookshelf) + ) + + else -> readBookResult.launch( + Intent( + this, + if (!book.isLocal && book.isImage && AppConfig.showMangaUi) ReadMangaActivity::class.java + else ReadBookActivity::class.java + ) + .putExtra("bookUrl", book.bookUrl) + .putExtra("inBookshelf", viewModel.inBookshelf) + .putExtra("chapterChanged", chapterChanged) + ) + } + } + + override val oldBook: Book? + get() = viewModel.bookData.value + + override fun changeTo(source: BookSource, book: Book, toc: List) { + viewModel.changeTo(source, book, toc) + } + + override fun coverChangeTo(coverUrl: String) { + viewModel.bookData.value?.let { book -> + book.customCoverUrl = coverUrl + showCover(book) + if (viewModel.inBookshelf) { + viewModel.saveBook(book) + } + } + } + + override fun upGroup(requestCode: Int, groupId: Long) { + upGroup(groupId) + viewModel.getBook()?.let { book -> + book.group = groupId + if (viewModel.inBookshelf) { + viewModel.saveBook(book) + } else if (groupId > 0) { + viewModel.addToBookshelf { + upTvBookshelf() + } + } + } + } + + private fun upWaitDialogStatus(isShow: Boolean) { + val showText = "Loading....." + if (isShow) { + waitDialog.run { + setText(showText) + show() + } + } else { + waitDialog.dismiss() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt new file mode 100755 index 000000000..c161a7993 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -0,0 +1,529 @@ +package io.legado.app.ui.book.info + +import android.app.Application +import android.content.Intent +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoBooksDirException +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.getExportFileName +import io.legado.app.help.book.getRemoteUrl +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isNotShelf +import io.legado.app.help.book.isSameNameAuthor +import io.legado.app.help.book.isWebFile +import io.legado.app.help.book.removeType +import io.legado.app.help.book.updateTo +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.webdav.ObjectNotFoundException +import io.legado.app.model.AudioPlay +import io.legado.app.model.BookCover +import io.legado.app.model.ReadBook +import io.legado.app.model.ReadManga +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.ArchiveUtils +import io.legado.app.utils.UrlUtil +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO + +class BookInfoViewModel(application: Application) : BaseViewModel(application) { + val bookData = MutableLiveData() + val chapterListData = MutableLiveData>() + val webFiles = mutableListOf() + var inBookshelf = false + var bookSource: BookSource? = null + private var changeSourceCoroutine: Coroutine<*>? = null + val waitDialogData = MutableLiveData() + val actionLive = MutableLiveData() + + fun initData(intent: Intent) { + execute { + val name = intent.getStringExtra("name") ?: "" + val author = intent.getStringExtra("author") ?: "" + val bookUrl = intent.getStringExtra("bookUrl") ?: "" + appDb.bookDao.getBook(name, author)?.let { + inBookshelf = !it.isNotShelf + upBook(it) + return@execute + } + if (bookUrl.isNotBlank()) { + appDb.bookDao.getBook(bookUrl)?.let { + inBookshelf = !it.isNotShelf + upBook(it) + return@execute + } + appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let { + upBook(it) + return@execute + } + } + appDb.searchBookDao.getFirstByNameAuthor(name, author)?.toBook()?.let { + upBook(it) + return@execute + } + throw NoStackTraceException("未找到书籍") + }.onError { + AppLog.put(it.localizedMessage, it) + context.toastOnUi(it.localizedMessage) + } + } + + fun upBook(intent: Intent) { + execute { + val name = intent.getStringExtra("name") ?: "" + val author = intent.getStringExtra("author") ?: "" + appDb.bookDao.getBook(name, author)?.let { book -> + upBook(book) + } + } + } + + private fun upBook(book: Book) { + execute { + bookData.postValue(book) + upCoverByRule(book) + bookSource = if (book.isLocal) null else + appDb.bookSourceDao.getBookSource(book.origin) + if (book.tocUrl.isEmpty() && !book.isLocal) { + loadBookInfo(book, runPreUpdateJs = inBookshelf) + } else { + val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl) + if (chapterList.isNotEmpty()) { + chapterListData.postValue(chapterList) + } else { + loadChapter(book) + } + } + } + } + + private fun upCoverByRule(book: Book) { + execute { + if (book.coverUrl.isNullOrBlank() && book.customCoverUrl.isNullOrBlank()) { + val coverUrl = BookCover.searchCover(book) + if (coverUrl.isNullOrBlank()) { + return@execute + } + book.customCoverUrl = coverUrl + bookData.postValue(book) + if (inBookshelf) { + saveBook(book) + } + } + } + } + + fun refreshBook(book: Book) { + executeLazy(executeContext = IO) { + if (book.isLocal) { + book.tocUrl = "" + book.getRemoteUrl()?.let { + val bookWebDav = AppWebDav.defaultBookWebDav + ?: throw NoStackTraceException("webDav没有配置") + val remoteBook = bookWebDav.getRemoteBook(it) + if (remoteBook == null) { + book.origin = BookType.localTag + } else if (remoteBook.lastModify > book.lastCheckTime) { + val uri = bookWebDav.downloadRemoteBook(remoteBook) + book.bookUrl = if (uri.isContentScheme()) uri.toString() else uri.path!! + book.lastCheckTime = remoteBook.lastModify + } + } + } else { + val bs = bookSource ?: return@executeLazy + if (book.originName != bs.bookSourceName) { + book.originName = bs.bookSourceName + } + } + }.onError { + when (it) { + is ObjectNotFoundException -> { + book.origin = BookType.localTag + } + + else -> { + AppLog.put("下载远程书籍<${book.name}>失败", it) + } + } + }.onFinally { + loadBookInfo(book, false) + }.start() + } + + fun loadBookInfo( + book: Book, + canReName: Boolean = true, + runPreUpdateJs: Boolean = true, + scope: CoroutineScope = viewModelScope + ) { + if (book.isLocal) { + LocalBook.upBookInfo(book) + bookData.postValue(book) + loadChapter(book) + } else { + val bookSource = bookSource ?: let { + chapterListData.postValue(emptyList()) + context.toastOnUi(R.string.error_no_source) + return + } + WebBook.getBookInfo(scope, bookSource, book, canReName = canReName) + .onSuccess(IO) { + val dbBook = appDb.bookDao.getBook(book.name, book.author) + if (!inBookshelf && dbBook != null && !dbBook.isNotShelf && dbBook.origin == book.origin) { + /** + * book 来自搜索时(inBookshelf == false),搜索的书名不存在于书架,但是加载详情后,书名更新,存在同名书籍 + * 此时 book 的数据会与数据库中的不同,需要更新 #3652 #4619 + * book 加载详情后虽然书名作者相同,但是又可能不是数据库中(书源不同)的那本书 #3149 + */ + dbBook.updateTo(it) + inBookshelf = true + } + bookData.postValue(it) + if (inBookshelf) { + it.save() + } + if (it.isWebFile) { + loadWebFile(it) + } else { + loadChapter(it, runPreUpdateJs) + } + }.onError { + AppLog.put("获取书籍信息失败\n${it.localizedMessage}", it) + context.toastOnUi(R.string.error_get_book_info) + } + } + } + + private fun loadChapter( + book: Book, + runPreUpdateJs: Boolean = true, + scope: CoroutineScope = viewModelScope + ) { + if (book.isLocal) { + execute(scope) { + LocalBook.getChapterList(book).let { + appDb.bookDao.update(book) + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*it.toTypedArray()) + ReadBook.onChapterListUpdated(book) + bookData.postValue(book) + chapterListData.postValue(it) + } + }.onError { + context.toastOnUi("LoadTocError:${it.localizedMessage}") + } + } else { + val bookSource = bookSource ?: let { + chapterListData.postValue(emptyList()) + context.toastOnUi(R.string.error_no_source) + return + } + val oldBook = book.copy() + WebBook.getChapterList(scope, bookSource, book, runPreUpdateJs) + .onSuccess(IO) { + if (inBookshelf) { + appDb.bookDao.replace(oldBook, book) + /** + * runPreUpdateJs 有可能会修改 book 的 bookUrl + */ + if (oldBook.bookUrl != book.bookUrl) { + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(oldBook.bookUrl) + appDb.bookChapterDao.insert(*it.toTypedArray()) + ReadBook.onChapterListUpdated(book) + } + bookData.postValue(book) + chapterListData.postValue(it) + }.onError { + chapterListData.postValue(emptyList()) + AppLog.put("获取目录失败\n${it.localizedMessage}", it) + context.toastOnUi(R.string.error_get_chapter_list) + } + } + } + + + fun loadGroup(groupId: Long, success: ((groupNames: String?) -> Unit)) { + execute { + appDb.bookGroupDao.getGroupNames(groupId).joinToString(",") + }.onSuccess { + success.invoke(it) + } + } + + private fun loadWebFile(book: Book) { + execute { + webFiles.clear() + val fileNameNoExtension = if (book.author.isBlank()) book.name + else "${book.name} 作者:${book.author}" + book.downloadUrls!!.map { + val analyzeUrl = AnalyzeUrl( + it, source = bookSource, + coroutineContext = coroutineContext + ) + val mFileName = UrlUtil.getFileName(analyzeUrl) + ?: "${fileNameNoExtension}.${analyzeUrl.type}" + WebFile(it, mFileName) + } + }.onError { + context.toastOnUi("LoadWebFileError\n${it.localizedMessage}") + }.onSuccess { + webFiles.addAll(it) + } + } + + /* 导入或者下载在线文件 */ + fun importOrDownloadWebFile(webFile: WebFile, success: ((T) -> Unit)?) { + bookSource ?: return + execute { + waitDialogData.postValue(true) + if (webFile.isSupported) { + val book = LocalBook.importFileOnLine( + webFile.url, + bookData.value!!.getExportFileName(webFile.suffix), + bookSource + ) + changeToLocalBook(book) + } else { + LocalBook.saveBookFile( + webFile.url, + bookData.value!!.getExportFileName(webFile.suffix), + bookSource + ) + } + }.onSuccess { + @Suppress("unchecked_cast") + success?.invoke(it as T) + }.onError { + when (it) { + is NoBooksDirException -> actionLive.postValue("selectBooksDir") + else -> { + AppLog.put("ImportWebFileError\n${it.localizedMessage}", it) + context.toastOnUi("ImportWebFileError\n${it.localizedMessage}") + webFiles.remove(webFile) + } + } + }.onFinally { + waitDialogData.postValue(false) + } + } + + fun getArchiveFilesName(archiveFileUri: Uri, onSuccess: (List) -> Unit) { + execute { + ArchiveUtils.getArchiveFilesName(archiveFileUri) { + AppPattern.bookFileRegex.matches(it) + } + }.onError { + AppLog.put("getArchiveEntriesName Error:\n${it.localizedMessage}", it) + context.toastOnUi("getArchiveEntriesName Error:\n${it.localizedMessage}") + }.onSuccess { + onSuccess.invoke(it) + } + } + + fun importArchiveBook( + archiveFileUri: Uri, + archiveEntryName: String, + success: ((Book) -> Unit)? = null + ) { + execute { + val suffix = archiveEntryName.substringAfterLast(".") + LocalBook.importArchiveFile( + archiveFileUri, + bookData.value!!.getExportFileName(suffix) + ) { + it.contains(archiveEntryName) + }.first() + }.onSuccess { + val book = changeToLocalBook(it) + success?.invoke(book) + }.onError { + AppLog.put("importArchiveBook Error:\n${it.localizedMessage}", it) + context.toastOnUi("importArchiveBook Error:\n${it.localizedMessage}") + } + } + + fun changeTo(source: BookSource, book: Book, toc: List) { + changeSourceCoroutine?.cancel() + changeSourceCoroutine = execute { + bookSource = source + bookData.value?.migrateTo(book, toc) + if (inBookshelf) { + book.removeType(BookType.updateError) + bookData.value?.delete() + appDb.bookDao.insert(book) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + } + bookData.postValue(book) + chapterListData.postValue(toc) + }.onFinally { + postEvent(EventBus.SOURCE_CHANGED, book.bookUrl) + } + } + + fun topBook() { + execute { + bookData.value?.let { book -> + val minOrder = appDb.bookDao.minOrder + book.order = minOrder - 1 + book.durChapterTime = System.currentTimeMillis() + appDb.bookDao.update(book) + } + } + } + + fun saveBook(book: Book?, success: (() -> Unit)? = null) { + book ?: return + execute { + if (book.order == 0) { + book.order = appDb.bookDao.minOrder - 1 + } + appDb.bookDao.getBook(book.name, book.author)?.let { + book.durChapterIndex = it.durChapterIndex + book.durChapterPos = it.durChapterPos + book.durChapterTitle = it.durChapterTitle + } + book.save() + if (ReadBook.book?.isSameNameAuthor(book) == true) { + ReadBook.book = book + } else if (AudioPlay.book?.isSameNameAuthor(book) == true) { + AudioPlay.book = book + } + }.onSuccess { + success?.invoke() + } + } + + fun saveChapterList(success: (() -> Unit)?) { + execute { + chapterListData.value?.let { + appDb.bookChapterDao.insert(*it.toTypedArray()) + } + }.onSuccess { + success?.invoke() + } + } + + fun addToBookshelf(success: (() -> Unit)?) { + execute { + bookData.value?.let { book -> + book.removeType(BookType.notShelf) + if (book.order == 0) { + book.order = appDb.bookDao.minOrder - 1 + } + appDb.bookDao.getBook(book.name, book.author)?.let { + book.durChapterIndex = it.durChapterIndex + book.durChapterPos = it.durChapterPos + book.durChapterTitle = it.durChapterTitle + } + if (ReadBook.book?.isSameNameAuthor(book) == true) { + ReadBook.book = book + } else if (AudioPlay.book?.isSameNameAuthor(book) == true) { + AudioPlay.book = book + } + book.save() + } + chapterListData.value?.let { + appDb.bookChapterDao.insert(*it.toTypedArray()) + } + inBookshelf = true + }.onSuccess { + success?.invoke() + } + } + + fun getBook(toastNull: Boolean = true): Book? { + val book = bookData.value + if (toastNull && book == null) { + context.toastOnUi("book is null") + } + return book + } + + fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) { + execute { + bookData.value?.let { + it.delete() + inBookshelf = false + if (it.isLocal) { + LocalBook.deleteBook(it, deleteOriginal) + } + } + }.onSuccess { + success?.invoke() + } + } + + fun clearCache() { + execute { + BookHelp.clearCache(bookData.value!!) + if (ReadBook.book?.bookUrl == bookData.value!!.bookUrl) { + ReadBook.clearTextChapter() + } + if (ReadManga.book?.bookUrl == bookData.value!!.bookUrl) { + ReadManga.clearMangaChapter() + } + }.onSuccess { + context.toastOnUi(R.string.clear_cache_success) + }.onError { + context.toastOnUi("清理缓存出错\n${it.localizedMessage}") + } + } + + fun upEditBook() { + bookData.value?.let { + appDb.bookDao.getBook(it.bookUrl)?.let { book -> + bookData.postValue(book) + } + } + } + + private fun changeToLocalBook(localBook: Book): Book { + return LocalBook.mergeBook(localBook, bookData.value).let { + bookData.postValue(it) + loadChapter(it) + inBookshelf = true + it + } + } + + data class WebFile( + val url: String, + val name: String, + ) { + + override fun toString(): String { + return name + } + + // 后缀 + val suffix: String = UrlUtil.getSuffix(name) + + // txt epub umd pdf等文件 + val isSupported: Boolean = AppPattern.bookFileRegex.matches(name) + + // 压缩包形式的txt epub umd pdf文件 + val isSupportDecompress: Boolean = AppPattern.archiveFileRegex.matches(name) + + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt new file mode 100755 index 000000000..b02f599ca --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt @@ -0,0 +1,171 @@ +package io.legado.app.ui.book.info.edit + +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.core.view.WindowInsetsCompat +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.BookType +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ActivityBookInfoEditBinding +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.addType +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isImage +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.removeType +import io.legado.app.ui.book.changecover.ChangeCoverDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.inputStream +import io.legado.app.utils.readUri +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.init.appCtx +import splitties.views.bottomPadding +import java.io.FileOutputStream + +class BookInfoEditActivity : + VMBaseActivity(), + ChangeCoverDialog.CallBack { + + private val selectCover = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + coverChangeTo(uri) + } + } + + override val binding by viewBinding(ActivityBookInfoEditBinding::inflate) + override val viewModel by viewModels() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.bookData.observe(this) { upView(it) } + if (viewModel.bookData.value == null) { + intent.getStringExtra("bookUrl")?.let { + viewModel.loadBook(it) + } + } + initView() + initEvent() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_info_edit, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> saveData() + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + binding.root.setOnApplyWindowInsetsListenerCompat { view, windowInsets -> + val typeMask = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() + val insets = windowInsets.getInsets(typeMask) + view.bottomPadding = insets.bottom + windowInsets + } + } + + private fun initEvent() = binding.run { + tvChangeCover.setOnClickListener { + viewModel.bookData.value?.let { + showDialogFragment( + ChangeCoverDialog(it.name, it.author) + ) + } + } + tvSelectCover.setOnClickListener { + selectCover.launch { + mode = HandleFileContract.IMAGE + } + } + tvRefreshCover.setOnClickListener { + viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() + upCover() + } + } + + private fun upView(book: Book) = binding.run { + tieBookName.setText(book.name) + tieBookAuthor.setText(book.author) + spType.setSelection( + when { + book.isImage -> 2 + book.isAudio -> 1 + else -> 0 + } + ) + tieCoverUrl.setText(book.getDisplayCover()) + tieBookIntro.setText(book.getDisplayIntro()) + upCover() + } + + private fun upCover() { + viewModel.book?.let { + binding.ivCover.load(it.getDisplayCover(), it.name, it.author, false, it.origin) + } + } + + private fun saveData() = binding.run { + val book = viewModel.book ?: return@run + val oldBook = book.copy() + book.name = tieBookName.text?.toString() ?: "" + book.author = tieBookAuthor.text?.toString() ?: "" + val local = if (book.isLocal) BookType.local else 0 + val bookType = when (spType.selectedItemPosition) { + 2 -> BookType.image or local + 1 -> BookType.audio or local + else -> BookType.text or local + } + book.removeType(BookType.local, BookType.image, BookType.audio, BookType.text) + book.addType(bookType) + val customCoverUrl = tieCoverUrl.text?.toString() + book.customCoverUrl = if (customCoverUrl == book.coverUrl) null else customCoverUrl + val customIntro = tieBookIntro.text?.toString() + book.customIntro = if (customIntro == book.intro) null else customIntro + BookHelp.updateCacheFolder(oldBook, book) + viewModel.saveBook(book) { + setResult(RESULT_OK) + finish() + } + } + + override fun coverChangeTo(coverUrl: String) { + viewModel.book?.customCoverUrl = coverUrl + binding.tieCoverUrl.setText(coverUrl) + upCover() + } + + private fun coverChangeTo(uri: Uri) { + readUri(uri) { fileDoc, inputStream -> + runCatching { + inputStream.use { + var file = this.externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val fileName = uri.inputStream(this).getOrThrow().use { + MD5Utils.md5Encode(it) + ".$suffix" + } + file = FileUtils.createFileIfNotExist(file, "covers", fileName) + FileOutputStream(file).use { outputStream -> + inputStream.copyTo(outputStream) + } + coverChangeTo(file.absolutePath) + } + }.onFailure { + appCtx.toastOnUi(it.localizedMessage) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt new file mode 100755 index 000000000..ebb4e6cec --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt @@ -0,0 +1,41 @@ +package io.legado.app.ui.book.info.edit + +import android.app.Application +import android.database.sqlite.SQLiteConstraintException +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.model.ReadBook + +class BookInfoEditViewModel(application: Application) : BaseViewModel(application) { + var book: Book? = null + val bookData = MutableLiveData() + + fun loadBook(bookUrl: String) { + execute { + book = appDb.bookDao.getBook(bookUrl) + book?.let { + bookData.postValue(it) + } + } + } + + fun saveBook(book: Book, success: (() -> Unit)?) { + execute { + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.book = book + } + appDb.bookDao.update(book) + }.onSuccess { + success?.invoke() + }.onError { + if (it is SQLiteConstraintException) { + AppLog.put("书籍信息保存失败,存在相同书名作者书籍\n$it", it, true) + } else { + AppLog.put("书籍信息保存失败\n$it", it, true) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt new file mode 100755 index 000000000..a6d26a42f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manage/BookAdapter.kt @@ -0,0 +1,239 @@ +package io.legado.app.ui.book.manage + +import android.annotation.SuppressLint +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ItemArrangeBookBinding +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import java.util.Collections + +class BookAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context), + + ItemTouchCallback.Callback { + val groupRequestCode = 12 + private val selectedBooks: HashSet = hashSetOf() + var actionItem: Book? = null + + val selection: List + get() { + return getItems().filter { + selectedBooks.contains(it) + } + } + + override fun getViewBinding(parent: ViewGroup): ItemArrangeBookBinding { + return ItemArrangeBookBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { + callBack.upSelectCount() + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemArrangeBookBinding, + item: Book, + payloads: MutableList + ) { + binding.apply { + root.setBackgroundColor(context.backgroundColor) + tvName.text = item.name + tvAuthor.text = item.author + tvAuthor.visibility = if (item.author.isEmpty()) View.GONE else View.VISIBLE + tvGroupS.text = getGroupName(item.group) + checkbox.isChecked = selectedBooks.contains(item) + if (item.isLocal) { + tvOrigin.setText(R.string.local_book) + } else { + tvOrigin.text = item.originName + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemArrangeBookBinding) { + binding.apply { + checkbox.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + if (isChecked) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + callBack.upSelectCount() + } + } + root.setOnClickListener { + getItem(holder.layoutPosition)?.let { + checkbox.isChecked = !checkbox.isChecked + if (checkbox.isChecked) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + callBack.upSelectCount() + } + } + if (AppConfig.openBookInfoByClickTitle) { + tvName.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.openBook(it) + } + } + } + tvDelete.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.deleteBook(it) + } + } + tvGroup.setOnClickListener { + getItem(holder.layoutPosition)?.let { + actionItem = it + callBack.selectGroup(groupRequestCode, it.group) + } + } + } + } + + @SuppressLint("NotifyDataSetChanged") + fun selectAll(selectAll: Boolean) { + if (selectAll) { + getItems().forEach { + selectedBooks.add(it) + } + } else { + selectedBooks.clear() + } + notifyDataSetChanged() + callBack.upSelectCount() + } + + @SuppressLint("NotifyDataSetChanged") + fun revertSelection() { + getItems().forEach { + if (selectedBooks.contains(it)) { + selectedBooks.remove(it) + } else { + selectedBooks.add(it) + } + } + notifyDataSetChanged() + callBack.upSelectCount() + } + + fun checkSelectedInterval() { + val selectedPosition = linkedSetOf() + getItems().forEachIndexed { index, it -> + if (selectedBooks.contains(it)) { + selectedPosition.add(index) + } + } + val minPosition = Collections.min(selectedPosition) + val maxPosition = Collections.max(selectedPosition) + val itemCount = maxPosition - minPosition + 1 + for (i in minPosition..maxPosition) { + getItem(i)?.let { + selectedBooks.add(it) + } + } + notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null))) + callBack.upSelectCount() + } + + private fun getGroupList(groupId: Long): List { + val groupNames = arrayListOf() + callBack.groupList.forEach { + if (it.groupId > 0 && it.groupId and groupId > 0) { + groupNames.add(it.groupName) + } + } + return groupNames + } + + private fun getGroupName(groupId: Long): String { + val groupNames = getGroupList(groupId) + if (groupNames.isEmpty()) { + return "" + } + return groupNames.joinToString(",") + } + + private var isMoved = false + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.order == targetItem.order) { + for ((index, item) in getItems().withIndex()) { + item.order = index + 1 + } + } else { + val pos = srcItem.order + srcItem.order = targetItem.order + targetItem.order = pos + } + } + swapItem(srcPosition, targetPosition) + isMoved = true + return true + } + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (isMoved) { + callBack.updateBook(*getItems().toTypedArray()) + } + isMoved = false + } + + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selectedBooks + } + + override fun getItemId(position: Int): Book { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upSelectCount() + return true + } + return false + } + } + + interface CallBack { + val groupList: List + + fun upSelectCount() + + fun updateBook(vararg book: Book) + + fun deleteBook(book: Book) + + fun selectGroup(requestCode: Int, groupId: Long) + + fun openBook(book: Book) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt new file mode 100755 index 000000000..464848f09 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt @@ -0,0 +1,418 @@ +package io.legado.app.ui.book.manage + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.CheckBox +import android.widget.LinearLayout +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.ActivityArrangeBookBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.book.contains +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.book.group.GroupManageDialog +import io.legado.app.ui.book.group.GroupSelectDialog +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.cnCompare +import io.legado.app.utils.dpToPx +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.math.max + +/** + * 书架管理 + */ +class BookshelfManageActivity : + VMBaseActivity(), + PopupMenu.OnMenuItemClickListener, + SelectActionBar.CallBack, + BookAdapter.CallBack, + SourcePickerDialog.Callback, + GroupSelectDialog.CallBack { + + override val binding by viewBinding(ActivityArrangeBookBinding::inflate) + override val viewModel by viewModels() + override val groupList: ArrayList = arrayListOf() + private val groupRequestCode = 22 + private val addToGroupRequestCode = 34 + private val adapter by lazy { BookAdapter(this, this) } + private val itemTouchCallback by lazy { ItemTouchCallback(adapter) } + private var booksFlowJob: Job? = null + private var menu: Menu? = null + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var books: List? = null + private val waitDialog by lazy { WaitDialog(this) } + private val exportDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + sendToClip(uri.toString()) + } + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.groupId = intent.getLongExtra("groupId", -1) + lifecycleScope.launch { + viewModel.groupName = withContext(IO) { + appDb.bookGroupDao.getByID(viewModel.groupId)?.groupName + ?: getString(R.string.no_group) + } + upTitle() + } + initSearchView() + initRecyclerView() + initOtherView() + initGroupData() + upBookDataByGroupId() + } + + override fun observeLiveBus() { + viewModel.batchChangeSourceState.observe(this) { + if (it) { + waitDialog.setText(R.string.change_source_batch) + waitDialog.show() + } else { + waitDialog.dismiss() + } + } + viewModel.batchChangeSourceProcessLiveData.observe(this) { + waitDialog.setText(it) + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.bookshelf_manage, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + this.menu = menu + menu.findItem(R.id.menu_open_book_info_by_click_title)?.isChecked = + AppConfig.openBookInfoByClickTitle + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickSelectBarMainAction() { + selectGroup(groupRequestCode, 0) + } + + private fun upTitle() { + searchView.queryHint = getString(R.string.screen) + " • " + viewModel.groupName + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + upBookData() + return false + } + + }) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter + itemTouchCallback.isCanDrag = AppConfig.bookshelfSort == 3 + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + // When this page is opened, it is in selection mode + dragSelectTouchHelper.activeSlideSelect() + // Note: need judge selection first, so add ItemTouchHelper after it. + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initOtherView() { + binding.selectActionBar.setMainActionText(R.string.move_to_group) + binding.selectActionBar.inflateMenu(R.menu.bookshelf_menage_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + waitDialog.setOnCancelListener { + viewModel.batchChangeSourceCoroutine?.cancel() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun initGroupData() { + lifecycleScope.launch { + appDb.bookGroupDao.flowAll().catch { + AppLog.put("书架管理界面获取分组数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { + groupList.clear() + groupList.addAll(it) + adapter.notifyDataSetChanged() + upMenu() + } + } + } + + private fun upBookDataByGroupId() { + booksFlowJob?.cancel() + booksFlowJob = lifecycleScope.launch { + val bookSort = AppConfig.getBookSortByGroupId(viewModel.groupId) + appDb.bookDao.flowByGroup(viewModel.groupId).map { list -> + when (bookSort) { + 1 -> list.sortedByDescending { + it.latestChapterTime + } + + 2 -> list.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + + 3 -> list.sortedBy { + it.order + } + + 4 -> list.sortedByDescending { + max(it.latestChapterTime, it.durChapterTime) + } + + else -> list.sortedByDescending { + it.durChapterTime + } + } + }.catch { + AppLog.put("书架管理界面获取书籍列表失败\n${it.localizedMessage}", it) + }.flowOn(IO) + .conflate().collect { + books = it + upBookData() + itemTouchCallback.isCanDrag = bookSort == 3 + } + } + } + + private fun upBookData() { + books?.let { books -> + val searchKey = searchView.query + if (searchKey.isNullOrEmpty()) { + adapter.setItems(books) + } else { + books.filter { + it.contains(searchKey.toString()) + }.let { + adapter.setItems(it) + } + } + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_group_manage -> showDialogFragment() + R.id.menu_open_book_info_by_click_title -> { + AppConfig.openBookInfoByClickTitle = !item.isChecked + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + + R.id.menu_export_all_use_book_source -> viewModel.saveAllUseBookSourceToFile { file -> + exportDir.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "bookSource.json", + file, + "application/json" + ) + } + } + + else -> if (item.groupId == R.id.menu_group) { + viewModel.groupName = item.title.toString() + upTitle() + viewModel.groupId = + appDb.bookGroupDao.getByName(item.title.toString())?.groupId ?: 0 + upBookDataByGroupId() + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_del_selection -> alertDelSelection() + R.id.menu_update_enable -> + viewModel.upCanUpdate(adapter.selection, true) + + R.id.menu_update_disable -> + viewModel.upCanUpdate(adapter.selection, false) + + R.id.menu_add_to_group -> selectGroup(addToGroupRequestCode, 0) + R.id.menu_change_source -> showDialogFragment() + R.id.menu_clear_cache -> viewModel.clearCache(adapter.selection) + R.id.menu_check_selected_interval -> adapter.checkSelectedInterval() + } + return false + } + + private fun upMenu() { + menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group) + groupList.forEach { bookGroup -> + subMenu.add(R.id.menu_group, bookGroup.order, Menu.NONE, bookGroup.groupName) + } + } + } + + private fun alertDelSelection() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + val checkBox = CheckBox(this@BookshelfManageActivity).apply { + setText(R.string.delete_book_file) + isChecked = LocalConfig.deleteBookOriginal + } + val view = LinearLayout(this@BookshelfManageActivity).apply { + setPadding(16.dpToPx(), 0, 16.dpToPx(), 0) + addView(checkBox) + } + customView { view } + okButton { + LocalConfig.deleteBookOriginal = checkBox.isChecked + viewModel.deleteBook(adapter.selection, checkBox.isChecked) + } + noButton() + } + } + + override fun selectGroup(requestCode: Int, groupId: Long) { + showDialogFragment( + GroupSelectDialog(groupId, requestCode) + ) + } + + override fun upGroup(requestCode: Int, groupId: Long) { + when (requestCode) { + groupRequestCode -> adapter.selection.let { books -> + val array = Array(books.size) { + books[it].copy(group = groupId) + } + viewModel.updateBook(*array) + } + + adapter.groupRequestCode -> { + adapter.actionItem?.let { + viewModel.updateBook(it.copy(group = groupId)) + } + } + + addToGroupRequestCode -> adapter.selection.let { books -> + val array = Array(books.size) { index -> + val book = books[index] + book.copy(group = book.group or groupId) + } + viewModel.updateBook(*array) + } + } + } + + override fun upSelectCount() { + binding.selectActionBar.upCountView(adapter.selection.size, adapter.getItems().size) + } + + override fun updateBook(vararg book: Book) { + viewModel.updateBook(*book) + } + + override fun deleteBook(book: Book) { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + var checkBox: CheckBox? = null + if (book.isLocal) { + checkBox = CheckBox(this@BookshelfManageActivity).apply { + setText(R.string.delete_book_file) + isChecked = LocalConfig.deleteBookOriginal + } + val view = LinearLayout(this@BookshelfManageActivity).apply { + setPadding(16.dpToPx(), 0, 16.dpToPx(), 0) + addView(checkBox) + } + customView { view } + } + okButton { + if (checkBox != null) { + LocalConfig.deleteBookOriginal = checkBox.isChecked + } + viewModel.deleteBook(listOf(book), LocalConfig.deleteBookOriginal) + } + } + } + + override fun openBook(book: Book) { + startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + } + } + + override fun sourceOnClick(source: BookSource) { + viewModel.changeSource(adapter.selection, source) + viewModel.batchChangeSourceState.value = true + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt new file mode 100755 index 000000000..bbdeceab0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt @@ -0,0 +1,131 @@ +package io.legado.app.ui.book.manage + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.removeType +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeToOutputStream +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import java.io.File + + +class BookshelfManageViewModel(application: Application) : BaseViewModel(application) { + var groupId: Long = -1L + var groupName: String? = null + val batchChangeSourceState = MutableLiveData() + val batchChangeSourceProcessLiveData = MutableLiveData() + var batchChangeSourceCoroutine: Coroutine? = null + + fun upCanUpdate(books: List, canUpdate: Boolean) { + execute { + val array = Array(books.size) { + books[it].copy(canUpdate = canUpdate).apply { + if (!canUpdate) { + removeType(BookType.updateError) + } + } + } + appDb.bookDao.update(*array) + } + } + + fun updateBook(vararg book: Book) { + execute { + appDb.bookDao.update(*book) + } + } + + fun deleteBook(books: List, deleteOriginal: Boolean = false) { + execute { + appDb.bookDao.delete(*books.toTypedArray()) + books.forEach { + if (it.isLocal) { + LocalBook.deleteBook(it, deleteOriginal) + } + } + } + } + + fun saveAllUseBookSourceToFile(success: (file: File) -> Unit) { + execute { + val path = "${context.filesDir}/shareBookSource.json" + FileUtils.delete(path) + val file = FileUtils.createFileWithReplace(path) + val sources = appDb.bookDao.getAllUseBookSource() + file.outputStream().buffered().use { + GSON.writeToOutputStream(it, sources) + } + file + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.stackTraceStr) + } + } + + fun changeSource(books: List, source: BookSource) { + batchChangeSourceCoroutine?.cancel() + batchChangeSourceCoroutine = execute { + val changeSourceDelay = AppConfig.batchChangeSourceDelay * 1000L + books.forEachIndexed { index, book -> + batchChangeSourceProcessLiveData.postValue("${index + 1} / ${books.size}") + if (book.isLocal) return@forEachIndexed + if (book.origin == source.bookSourceUrl) return@forEachIndexed + val newBook = WebBook.preciseSearchAwait(source, book.name, book.author) + .onFailure { + AppLog.put("搜索书籍出错\n${it.localizedMessage}", it, true) + }.getOrNull() ?: return@forEachIndexed + kotlin.runCatching { + if (newBook.tocUrl.isEmpty()) { + WebBook.getBookInfoAwait(source, newBook) + } + }.onFailure { + AppLog.put("获取书籍详情出错\n${it.localizedMessage}", it, true) + return@forEachIndexed + } + WebBook.getChapterListAwait(source, newBook) + .onFailure { + AppLog.put("获取目录出错\n${it.localizedMessage}", it, true) + }.getOrNull()?.let { toc -> + book.migrateTo(newBook, toc) + book.removeType(BookType.updateError) + appDb.bookDao.insert(newBook) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + } + delay(changeSourceDelay) + } + }.onStart { + batchChangeSourceState.postValue(true) + }.onFinally { + batchChangeSourceState.postValue(false) + } + } + + fun clearCache(books: List) { + execute { + books.forEach { + BookHelp.clearCache(it) + } + }.onSuccess { + context.toastOnUi(R.string.clear_cache_success) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt b/app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt new file mode 100755 index 000000000..9bd7a7d7c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manage/SourcePickerDialog.kt @@ -0,0 +1,161 @@ +package io.legado.app.ui.book.manage + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar +import androidx.core.view.setPadding +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.databinding.DialogSourcePickerBinding +import io.legado.app.databinding.Item1lineTextBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.widget.number.NumberPickerDialog +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import splitties.views.onClick + +/** + * 书源选择 + */ +class SourcePickerDialog : BaseDialogFragment(R.layout.dialog_source_picker), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogSourcePickerBinding::bind) + private val searchView: SearchView by lazy { + binding.toolBar.findViewById(R.id.search_view) + } + private val toolBar: Toolbar by lazy { + binding.toolBar.toolbar + } + private val adapter by lazy { + SourceAdapter(requireContext()) + } + private var sourceFlowJob: Job? = null + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + initData() + initMenu() + } + + private fun initView() { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = "选择书源" + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search_book_source) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + initData(newText) + return false + } + }) + } + + private fun initData(searchKey: String? = null) { + sourceFlowJob?.cancel() + sourceFlowJob = lifecycleScope.launch { + when { + searchKey.isNullOrEmpty() -> appDb.bookSourceDao.flowEnabled() + else -> appDb.bookSourceDao.flowSearchEnabled(searchKey) + }.catch { + AppLog.put("书源选择界面获取书源数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + private fun initMenu() { + toolBar.setOnMenuItemClickListener(this) + toolBar.inflateMenu(R.menu.source_picker) + toolBar.menu.applyTint(requireContext()) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_change_source_delay -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.change_source_delay)) + .setMaxValue(9999) + .setMinValue(0) + .setValue(AppConfig.batchChangeSourceDelay) + .show { + AppConfig.batchChangeSourceDelay = it + } + } + return true + } + + inner class SourceAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding { + return Item1lineTextBinding.inflate(inflater, parent, false).apply { + root.setPadding(16.dpToPx()) + } + } + + override fun convert( + holder: ItemViewHolder, + binding: Item1lineTextBinding, + item: BookSourcePart, + payloads: MutableList + ) { + binding.textView.text = item.getDisPlayNameGroup() + } + + override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) { + binding.root.onClick { + getItemByLayoutPosition(holder.layoutPosition)?.let { + it.getBookSource()?.let { source -> + callback?.sourceOnClick(source) + } + dismissAllowingStateLoss() + } + } + } + + } + + private val callback: Callback? + get() { + return (parentFragment as? Callback) ?: activity as? Callback + } + + interface Callback { + fun sourceOnClick(source: BookSource) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt b/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt new file mode 100755 index 000000000..0fcda95b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt @@ -0,0 +1,856 @@ +package io.legado.app.ui.book.manga + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.WindowManager +import android.view.animation.LinearInterpolator +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.PagerSnapHelper +import com.bumptech.glide.Glide +import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader +import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL +import com.bumptech.glide.util.FixedPreloadSizeProvider +import io.legado.app.BuildConfig +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.data.entities.BookSource +import io.legado.app.databinding.ActivityMangaBinding +import io.legado.app.databinding.ViewLoadMoreBinding +import io.legado.app.help.book.isImage +import io.legado.app.help.book.removeType +import io.legado.app.help.config.AppConfig +import io.legado.app.help.storage.Backup +import io.legado.app.lib.dialogs.alert +import io.legado.app.model.ReadManga +import io.legado.app.receiver.NetworkChangedListener +import io.legado.app.ui.book.changesource.ChangeBookSourceDialog +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.manga.config.MangaColorFilterConfig +import io.legado.app.ui.book.manga.config.MangaColorFilterDialog +import io.legado.app.ui.book.manga.config.MangaEpaperDialog +import io.legado.app.ui.book.manga.config.MangaFooterConfig +import io.legado.app.ui.book.manga.config.MangaFooterSettingDialog +import io.legado.app.ui.book.manga.entities.BaseMangaPage +import io.legado.app.ui.book.manga.entities.MangaPage +import io.legado.app.ui.book.manga.recyclerview.MangaAdapter +import io.legado.app.ui.book.manga.recyclerview.MangaLayoutManager +import io.legado.app.ui.book.manga.recyclerview.ScrollTimer +import io.legado.app.ui.book.read.MangaMenu +import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED +import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.widget.number.NumberPickerDialog +import io.legado.app.ui.widget.recycler.LoadMoreView +import io.legado.app.utils.GSON +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.canScroll +import io.legado.app.utils.fastBinarySearch +import io.legado.app.utils.findCenterViewPosition +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.observeEvent +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.toggleSystemBar +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.text.DecimalFormat +import kotlin.math.ceil + +class ReadMangaActivity : VMBaseActivity(), + ReadManga.Callback, ChangeBookSourceDialog.CallBack, MangaMenu.CallBack, + MangaColorFilterDialog.Callback, ScrollTimer.ScrollCallback, MangaEpaperDialog.Callback { + + private val mLayoutManager by lazy { + MangaLayoutManager(this) + } + private val mAdapter: MangaAdapter by lazy { + MangaAdapter(this) + } + + private val mSizeProvider by lazy { + FixedPreloadSizeProvider(resources.displayMetrics.widthPixels, SIZE_ORIGINAL) + } + + private val mPagerSnapHelper: PagerSnapHelper by lazy { + PagerSnapHelper() + } + + private lateinit var mMangaFooterConfig: MangaFooterConfig + private val mLabelBuilder by lazy { StringBuilder() } + + private var mMenu: Menu? = null + + private var mRecyclerViewPreloader: RecyclerViewPreloader? = null + + private val networkChangedListener by lazy { + NetworkChangedListener(this) + } + + private var justInitData: Boolean = false + private var syncDialog: AlertDialog? = null + private val mScrollTimer by lazy { + ScrollTimer(this, binding.recyclerView, lifecycleScope).apply { + setSpeed(AppConfig.mangaAutoPageSpeed) + } + } + private var enableAutoScrollPage = false + private var enableAutoScroll = false + private val mLinearInterpolator by lazy { + LinearInterpolator() + } + + private val loadMoreView by lazy { + LoadMoreView(this).apply { + setBackgroundColor(getCompatColor(R.color.book_ant_10)) + setLoadingColor(R.color.white) + setLoadingTextColor(R.color.white) + } + } + + //打开目录返回选择章节返回结果 + private val tocActivity = registerForActivityResult(TocActivityResult()) { + it?.let { + viewModel.openChapter(it.first, it.second) + } + } + private val bookInfoActivity = + registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) { + if (it.resultCode == RESULT_OK) { + setResult(RESULT_DELETED) + super.finish() + } else { + ReadManga.loadOrUpContent() + } + } + override val binding by viewBinding(ActivityMangaBinding::inflate) + override val viewModel by viewModels() + private val loadingViewVisible get() = binding.flLoading.isVisible + private val df by lazy { + DecimalFormat("0.0%") + } + + override fun onCreate(savedInstanceState: Bundle?) { + upLayoutInDisplayCutoutMode() + super.onCreate(savedInstanceState) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + ReadManga.register(this) + upSystemUiVisibility(false) + initRecyclerView() + binding.tvRetry.setOnClickListener { + binding.llLoading.isVisible = true + binding.llRetry.isGone = true + ReadManga.loadOrUpContent() + } + binding.pbLoading.isVisible = !AppConfig.isEInkMode + mAdapter.addFooterView { + ViewLoadMoreBinding.bind(loadMoreView) + } + loadMoreView.setOnClickListener { + if (!loadMoreView.isLoading && ReadManga.hasNextChapter) { + loadMoreView.startLoad() + ReadManga.loadOrUpContent() + } + } + loadMoreView.gone() + mMangaFooterConfig = + GSON.fromJsonObject(AppConfig.mangaFooterConfig).getOrNull() + ?: MangaFooterConfig() + } + + override fun observeLiveBus() { + observeEvent(EventBus.UP_MANGA_CONFIG) { + mMangaFooterConfig = it + val item = mAdapter.getItem(binding.recyclerView.findCenterViewPosition()) + upInfoBar(item) + } + } + + private fun initRecyclerView() { + val mangaColorFilter = + GSON.fromJsonObject(AppConfig.mangaColorFilter).getOrNull() + ?: MangaColorFilterConfig() + mAdapter.run { + setMangaImageColorFilter(mangaColorFilter) + enableMangaEInk(AppConfig.enableMangaEInk, AppConfig.mangaEInkThreshold) + enableGray(AppConfig.enableMangaGray) + } + setHorizontalScroll(AppConfig.enableMangaHorizontalScroll) + binding.recyclerView.run { + adapter = mAdapter + itemAnimator = null + layoutManager = mLayoutManager + setHasFixedSize(true) + setDisableClickScroll(AppConfig.disableClickScroll) + setDisableMangaScale(AppConfig.disableMangaScale) + setRecyclerViewPreloader(AppConfig.mangaPreDownloadNum) + setPreScrollListener { _, _, _, position -> + if (mAdapter.isNotEmpty()) { + val item = mAdapter.getItem(position) + if (item is BaseMangaPage) { + if (ReadManga.durChapterIndex < item.chapterIndex) { + ReadManga.moveToNextChapter() + } else if (ReadManga.durChapterIndex > item.chapterIndex) { + ReadManga.moveToPrevChapter() + } else { + ReadManga.durChapterPos = item.index + ReadManga.curPageChanged() + } + if (item is MangaPage) { + binding.mangaMenu.upSeekBar(item.index, item.imageCount) + upInfoBar(item) + } + } + } + } + } + binding.webtoonFrame.run { + onTouchMiddle { + if (!binding.mangaMenu.isVisible && !loadingViewVisible) { + binding.mangaMenu.runMenuIn() + } + } + onNextPage { + scrollToNext() + } + onPrevPage { + scrollToPrev() + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + viewModel.initData(intent) + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + viewModel.initData(intent) + justInitData = true + } + + override fun upContent() { + lifecycleScope.launch { + setTitle(ReadManga.book?.name) + val data = withContext(IO) { ReadManga.mangaContents } + val pos = data.pos + val list = data.items + val curFinish = data.curFinish + val nextFinish = data.nextFinish + mAdapter.submitList(list) { + if (loadingViewVisible && curFinish) { + binding.infobar.isVisible = true + upInfoBar(list[pos]) + mLayoutManager.scrollToPositionWithOffset(pos, 0) + binding.flLoading.isGone = true + loadMoreView.visible() + binding.mangaMenu.upSeekBar( + ReadManga.durChapterPos, ReadManga.curMangaChapter!!.imageCount + ) + } + + if (curFinish) { + if (!ReadManga.hasNextChapter) { + loadMoreView.noMore("暂无章节了!") + } else if (nextFinish) { + loadMoreView.stopLoad() + } else { + loadMoreView.startLoad() + } + } + } + } + } + + private fun upInfoBar(page: Any?) { + if (page !is MangaPage) { + return + } + val chapterIndex = page.chapterIndex + val chapterSize = page.chapterSize + val chapterPos = page.index + val imageCount = page.imageCount + val chapterName = page.mChapterName + mMangaFooterConfig.run { + mLabelBuilder.clear() + binding.infobar.isGone = hideFooter + binding.infobar.textInfoAlignment = footerOrientation + + if (!hideChapterName) { + mLabelBuilder.append(chapterName).append(" ") + } + + if (!hidePageNumber) { + if (!hidePageNumberLabel) { + mLabelBuilder.append(getString(R.string.manga_check_page_number)) + } + mLabelBuilder.append("${chapterPos + 1}/${imageCount}").append(" ") + } + + if (!hideChapter) { + if (!hideChapterLabel) { + mLabelBuilder.append(getString(R.string.manga_check_chapter)) + } + mLabelBuilder.append("${chapterIndex + 1}/${chapterSize}").append(" ") + } + + if (!hideProgressRatio) { + if (!hideProgressRatioLabel) { + mLabelBuilder.append(getString(R.string.manga_check_progress)) + } + val percent = if (chapterSize == 0 || imageCount == 0 && chapterIndex == 0) { + "0.0%" + } else if (imageCount == 0) { + df.format((chapterIndex + 1.0f) / chapterSize.toDouble()) + } else { + var percent = + df.format( + chapterIndex * 1.0f / chapterSize + 1.0f / + chapterSize * (chapterPos + 1) / imageCount.toDouble() + ) + if (percent == "100.0%" && (chapterIndex + 1 != chapterSize || chapterPos + 1 != imageCount)) { + percent = "99.9%" + } + percent + } + mLabelBuilder.append(percent) + } + } + binding.infobar.update( + if (mLabelBuilder.isEmpty()) "" else mLabelBuilder.toString() + ) + } + + override fun onResume() { + super.onResume() + networkChangedListener.register() + networkChangedListener.onNetworkChanged = { + // 当网络是可用状态且无需初始化时同步进度(初始化中已有同步进度逻辑) + if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData && ReadManga.inBookshelf) { + ReadManga.syncProgress({ progress -> sureNewProgress(progress) }) + } + } + if (enableAutoScrollPage) { + mScrollTimer.isEnabledPage = true + } + if (enableAutoScroll) { + mScrollTimer.isEnabled = true + } + } + + override fun onPause() { + super.onPause() + if (ReadManga.inBookshelf) { + ReadManga.saveRead() + if (!BuildConfig.DEBUG) { + if (AppConfig.syncBookProgressPlus) { + ReadManga.syncProgress() + } else { + ReadManga.uploadProgress() + } + } + } + if (!BuildConfig.DEBUG) { + Backup.autoBack(this) + } + ReadManga.cancelPreDownloadTask() + networkChangedListener.unRegister() + mScrollTimer.isEnabledPage = false + mScrollTimer.isEnabled = false + } + + override fun loadFail(msg: String, retry: Boolean) { + lifecycleScope.launch { + if (loadingViewVisible) { + binding.llLoading.isGone = true + binding.llRetry.isVisible = true + binding.tvRetry.isVisible = retry + binding.tvMsg.text = msg + } else { + loadMoreView.error(null, "加载失败,点击重试") + } + } + } + + override fun onDestroy() { + ReadManga.unregister(this) + super.onDestroy() + } + + override fun onLowMemory() { + super.onLowMemory() + Glide.get(this).clearMemory() + } + + override fun sureNewProgress(progress: BookProgress) { + syncDialog?.dismiss() + syncDialog = alert(R.string.get_book_progress) { + setMessage(R.string.cloud_progress_exceeds_current) + okButton { + ReadManga.setProgress(progress) + } + noButton() + } + } + + override fun showLoading() { + lifecycleScope.launch { + binding.flLoading.isVisible = true + } + } + + override fun startLoad() { + lifecycleScope.launch { + loadMoreView.startLoad() + } + } + + override fun scrollBy(distance: Int) { + if (!binding.recyclerView.canScroll(1)) { + return + } + val time = ceil(16f / distance * 10000).toInt() + binding.recyclerView.smoothScrollBy(10000, 10000, mLinearInterpolator, time) + } + + override fun scrollPage() { + scrollToNext() + } + + override val oldBook: Book? + get() = ReadManga.book + + override fun changeTo(source: BookSource, book: Book, toc: List) { + if (book.isImage) { + binding.flLoading.isVisible = true + viewModel.changeTo(book, toc) + } else { + toastOnUi("所选择的源不是漫画源") + } + } + + override fun updateColorFilter(config: MangaColorFilterConfig) { + mAdapter.setMangaImageColorFilter(config) + updateWindowBrightness(config.l) + } + + @SuppressLint("StringFormatMatches") + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_manga, menu) + upMenu(menu) + return super.onCompatCreateOptionsMenu(menu) + } + + /** + * 菜单 + */ + @SuppressLint("StringFormatMatches", "NotifyDataSetChanged") + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_change_source -> { + binding.mangaMenu.runMenuOut() + ReadManga.book?.let { + showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) + } + } + + R.id.menu_catalog -> { + ReadManga.book?.let { + tocActivity.launch(it.bookUrl) + } + } + + R.id.menu_refresh -> { + binding.flLoading.isVisible = true + ReadManga.book?.let { + viewModel.refreshContentDur(it) + } + } + + R.id.menu_pre_manga_number -> { + showNumberPickerDialog( + 0, + getString(R.string.pre_download), + AppConfig.mangaPreDownloadNum + ) { + AppConfig.mangaPreDownloadNum = it + item.title = getString(R.string.pre_download_m, it) + setRecyclerViewPreloader(it) + } + } + + R.id.menu_disable_manga_scale -> { + item.isChecked = !item.isChecked + AppConfig.disableMangaScale = item.isChecked + setDisableMangaScale(item.isChecked) + } + + R.id.menu_disable_click_scroll -> { + item.isChecked = !item.isChecked + AppConfig.disableClickScroll = item.isChecked + setDisableClickScroll(item.isChecked) + } + + R.id.menu_enable_auto_page -> { + item.isChecked = !item.isChecked + val menuMangaAutoPageSpeed = mMenu?.findItem(R.id.menu_manga_auto_page_speed) + mScrollTimer.isEnabledPage = item.isChecked + menuMangaAutoPageSpeed?.isVisible = item.isChecked + enableAutoScrollPage = item.isChecked + enableAutoScroll = false + mScrollTimer.isEnabled = false + mMenu?.findItem(R.id.menu_enable_auto_scroll)?.isChecked = false + } + + R.id.menu_manga_auto_page_speed -> { + showNumberPickerDialog( + 1, getString(R.string.setting_manga_auto_page_speed), + AppConfig.mangaAutoPageSpeed + ) { + AppConfig.mangaAutoPageSpeed = it + item.title = getString(R.string.manga_auto_page_speed, it) + mScrollTimer.setSpeed(it) + if (enableAutoScrollPage) { + mScrollTimer.isEnabledPage = true + } + } + } + + R.id.menu_manga_footer_config -> { + showDialogFragment(MangaFooterSettingDialog()) + } + + R.id.menu_enable_horizontal_scroll -> { + item.isChecked = !item.isChecked + AppConfig.enableMangaHorizontalScroll = item.isChecked + mMenu?.findItem(R.id.menu_disable_horizontal_page_snap)?.isVisible = item.isChecked + setHorizontalScroll(item.isChecked) + mAdapter.notifyDataSetChanged() + } + + R.id.menu_manga_color_filter -> { + binding.mangaMenu.runMenuOut() + showDialogFragment(MangaColorFilterDialog()) + } + + R.id.menu_enable_auto_scroll -> { + item.isChecked = !item.isChecked + mScrollTimer.isEnabled = item.isChecked + mMenu?.findItem(R.id.menu_enable_auto_page)?.isChecked = false + enableAutoScroll = item.isChecked + enableAutoScrollPage = false + mScrollTimer.isEnabledPage = false + mMenu?.findItem(R.id.menu_manga_auto_page_speed)?.isVisible = item.isChecked + if (enableAutoScroll) { + mPagerSnapHelper.attachToRecyclerView(null) + } else if (AppConfig.enableMangaHorizontalScroll) { + mPagerSnapHelper.attachToRecyclerView(binding.recyclerView) + } + } + + R.id.menu_hide_manga_title -> { + item.isChecked = !item.isChecked + AppConfig.hideMangaTitle = item.isChecked + ReadManga.loadContent() + } + + R.id.menu_epaper_manga -> { + item.isChecked = !item.isChecked + AppConfig.enableMangaEInk = item.isChecked + mMenu?.findItem(R.id.menu_gray_manga)?.isChecked = false + AppConfig.enableMangaGray = false + mMenu?.findItem(R.id.menu_epaper_manga_setting)?.isVisible = item.isChecked + mAdapter.enableMangaEInk(item.isChecked, AppConfig.mangaEInkThreshold) + } + + R.id.menu_epaper_manga_setting -> { + showDialogFragment(MangaEpaperDialog()) + } + + R.id.menu_disable_horizontal_page_snap -> { + item.isChecked = !item.isChecked + AppConfig.disableHorizontalPageSnap = item.isChecked + if (item.isChecked) { + mPagerSnapHelper.attachToRecyclerView(null) + } else { + mPagerSnapHelper.attachToRecyclerView(binding.recyclerView) + } + } + + R.id.menu_disable_manga_page_anim -> { + item.isChecked = !item.isChecked + mMenu?.findItem(R.id.menu_disable_horizontal_page_snap)?.isVisible = !item.isChecked + AppConfig.disableMangaPageAnim = item.isChecked + if (item.isChecked) { + mPagerSnapHelper.attachToRecyclerView(null) + } else { + if (AppConfig.enableMangaHorizontalScroll && !AppConfig.disableHorizontalPageSnap) { + mPagerSnapHelper.attachToRecyclerView(binding.recyclerView) + } + } + } + + R.id.menu_gray_manga -> { + item.isChecked = !item.isChecked + AppConfig.enableMangaGray = item.isChecked + mMenu?.findItem(R.id.menu_epaper_manga)?.isChecked = false + AppConfig.enableMangaEInk = false + mMenu?.findItem(R.id.menu_epaper_manga_setting)?.isVisible = false + mAdapter.enableGray(item.isChecked) + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun openBookInfoActivity() { + ReadManga.book?.let { + bookInfoActivity.launch { + putExtra("name", it.name) + putExtra("author", it.author) + } + } + } + + override fun upSystemUiVisibility(menuIsVisible: Boolean) { + toggleSystemBar(menuIsVisible) + if (enableAutoScroll) { + mScrollTimer.isEnabled = !menuIsVisible + } + if (enableAutoScrollPage) { + mScrollTimer.isEnabledPage = !menuIsVisible + } + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val keyCode = event.keyCode + val action = event.action + val isDown = action == 0 + + if (keyCode == KeyEvent.KEYCODE_MENU) { + if (isDown && !binding.mangaMenu.canShowMenu) { + binding.mangaMenu.runMenuIn() + return true + } + if (!isDown && !binding.mangaMenu.canShowMenu) { + binding.mangaMenu.canShowMenu = true + return true + } + } + return super.dispatchKeyEvent(event) + } + + private fun setRecyclerViewPreloader(maxPreload: Int) { + if (mRecyclerViewPreloader != null) { + binding.recyclerView.removeOnScrollListener(mRecyclerViewPreloader!!) + } + mRecyclerViewPreloader = RecyclerViewPreloader( + Glide.with(this), mAdapter, mSizeProvider, maxPreload + ) + binding.recyclerView.addOnScrollListener(mRecyclerViewPreloader!!) + } + + private fun setHorizontalScroll(enable: Boolean) { + mAdapter.isHorizontal = enable + if (enable) { + if (!enableAutoScroll) { + if (AppConfig.disableHorizontalPageSnap || AppConfig.disableMangaPageAnim) { + mPagerSnapHelper.attachToRecyclerView(null) + } else { + mPagerSnapHelper.attachToRecyclerView(binding.recyclerView) + } + } + mLayoutManager.orientation = LinearLayoutManager.HORIZONTAL + } else { + mPagerSnapHelper.attachToRecyclerView(null) + mLayoutManager.orientation = LinearLayoutManager.VERTICAL + } + } + + @SuppressLint("StringFormatMatches") + private fun upMenu(menu: Menu) { + this.mMenu = menu + menu.findItem(R.id.menu_pre_manga_number).title = + getString(R.string.pre_download_m, AppConfig.mangaPreDownloadNum) + menu.findItem(R.id.menu_disable_manga_scale).isChecked = AppConfig.disableMangaScale + menu.findItem(R.id.menu_disable_click_scroll).isChecked = AppConfig.disableClickScroll + menu.findItem(R.id.menu_manga_auto_page_speed).title = + getString(R.string.manga_auto_page_speed, AppConfig.mangaAutoPageSpeed) + menu.findItem(R.id.menu_enable_horizontal_scroll).isChecked = + AppConfig.enableMangaHorizontalScroll + menu.findItem(R.id.menu_epaper_manga).isChecked = AppConfig.enableMangaEInk + menu.findItem(R.id.menu_epaper_manga_setting).isVisible = AppConfig.enableMangaEInk + menu.findItem(R.id.menu_disable_horizontal_page_snap).run { + isVisible = AppConfig.enableMangaHorizontalScroll && !AppConfig.disableMangaPageAnim + isChecked = AppConfig.disableHorizontalPageSnap || AppConfig.disableMangaPageAnim + } + menu.findItem(R.id.menu_disable_manga_page_anim).isChecked = AppConfig.disableMangaPageAnim + menu.findItem(R.id.menu_gray_manga).isChecked = AppConfig.enableMangaGray + } + + private fun setDisableMangaScale(disable: Boolean) { + binding.webtoonFrame.disableMangaScale = disable + binding.recyclerView.disableMangaScale = disable + if (disable) { + binding.recyclerView.resetZoom() + } + } + + private fun setDisableClickScroll(disable: Boolean) { + binding.webtoonFrame.disabledClickScroll = disable + } + + private fun upLayoutInDisplayCutoutMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.attributes = window.attributes.apply { + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + } + } + + private fun scrollToNext() { + scrollPageTo(1) + } + + private fun scrollToPrev() { + scrollPageTo(-1) + } + + private fun scrollPageTo(direction: Int) { + if (!binding.recyclerView.canScroll(direction)) { + return + } + var dx = 0 + var dy = 0 + if (AppConfig.enableMangaHorizontalScroll) { + dx = binding.recyclerView.run { + width - paddingStart - paddingEnd + } + } else { + dy = binding.recyclerView.run { + height - paddingTop - paddingBottom + } + } + dx *= direction + dy *= direction + if (AppConfig.disableMangaPageAnim) { + binding.recyclerView.scrollBy(dx, dy) + } else { + binding.recyclerView.smoothScrollBy(dx, dy) + } + } + + private fun showNumberPickerDialog( + min: Int, + title: String, + initValue: Int, + callback: (Int) -> Unit, + ) { + NumberPickerDialog(this) + .setTitle(title) + .setMaxValue(9999) + .setMinValue(min) + .setValue(initValue) + .show { + callback.invoke(it) + } + } + + override fun finish() { + val book = ReadManga.book ?: return super.finish() + + if (ReadManga.inBookshelf) { + return super.finish() + } + + if (!AppConfig.showAddToShelfAlert) { + viewModel.removeFromBookshelf { super.finish() } + } else { + alert(title = getString(R.string.add_to_bookshelf)) { + setMessage(getString(R.string.check_add_bookshelf, book.name)) + okButton { + ReadManga.book?.removeType(BookType.notShelf) + ReadManga.book?.save() + ReadManga.inBookshelf = true + setResult(RESULT_OK) + } + noButton { viewModel.removeFromBookshelf { super.finish() } } + } + } + } + + fun updateWindowBrightness(brightness: Int) { + val layoutParams = window.attributes + val normalizedBrightness = brightness.toFloat() / 255.0f + layoutParams.screenBrightness = normalizedBrightness.coerceIn(0f, 1f) + window.attributes = layoutParams + // 强制刷新屏幕 + window.decorView.postInvalidate() + } + + override fun skipToPage(index: Int) { + val durChapterIndex = ReadManga.durChapterIndex + val itemPos = mAdapter.getItems().fastBinarySearch { + val chapterIndex: Int + val pageIndex: Int + if (it is BaseMangaPage) { + chapterIndex = it.chapterIndex + pageIndex = it.index + } else { + error("unknown item type") + } + val delta = chapterIndex - durChapterIndex + if (delta != 0) { + delta + } else { + pageIndex - index + } + } + if (itemPos > -1) { + mLayoutManager.scrollToPositionWithOffset(itemPos, 0) + upInfoBar(mAdapter.getItem(itemPos)) + ReadManga.durChapterPos = index + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP -> { + scrollToPrev() + return true + } + + KeyEvent.KEYCODE_VOLUME_DOWN -> { + scrollToNext() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + override fun updateEepaper(value: Int) { + mAdapter.updateThreshold(value) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt b/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt new file mode 100755 index 000000000..a9157d059 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt @@ -0,0 +1,298 @@ +package io.legado.app.ui.book.manga + +import android.app.Application +import android.content.Intent +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isLocalModified +import io.legado.app.help.book.removeType +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.ReadManga +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.mapParallelSafe +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.take +import splitties.init.appCtx + +class ReadMangaViewModel(application: Application) : BaseViewModel(application) { + + private var changeSourceCoroutine: Coroutine<*>? = null + + /** + * 初始化 + */ + fun initData(intent: Intent, success: (() -> Unit)? = null) { + execute { + ReadManga.inBookshelf = intent.getBooleanExtra("inBookshelf", true) + ReadManga.chapterChanged = intent.getBooleanExtra("chapterChanged", false) + val bookUrl = intent.getStringExtra("bookUrl") + val book = when { + bookUrl.isNullOrEmpty() -> appDb.bookDao.lastReadBook + else -> appDb.bookDao.getBook(bookUrl) + } ?: ReadManga.book + when { + book != null -> initManga(book) + else -> { + ReadManga.loadFail(context.getString(R.string.no_book), false) + AppLog.put("未找到漫画书籍\nbookUrl:$bookUrl") + } + } + }.onSuccess { + success?.invoke() + }.onError { + val msg = "初始化数据失败\n${it.localizedMessage}" + AppLog.put(msg, it) + }.onFinally { + ReadManga.saveRead() + } + } + + private suspend fun initManga(book: Book) { + val isSameBook = ReadManga.book?.bookUrl == book.bookUrl + if (isSameBook) { + ReadManga.upData(book) + } else { + ReadManga.resetData(book) + } + if (!book.isLocal && book.tocUrl.isEmpty() && !loadBookInfo(book)) { + return + } + + if (book.isLocal && !checkLocalBookFileExist(book)) { + return + } + + if ((ReadManga.chapterSize == 0 || book.isLocalModified()) && !loadChapterListAwait(book)) { + return + } + + //开始加载内容 + if (!isSameBook) { + ReadManga.loadContent() + } else { + ReadManga.loadOrUpContent() + } + + if (ReadManga.chapterChanged) { + // 有章节跳转不同步阅读进度 + ReadManga.chapterChanged = false + } else if (ReadManga.inBookshelf) { + if (AppConfig.syncBookProgressPlus) { + ReadManga.syncProgress( + { progress -> ReadManga.mCallback?.sureNewProgress(progress) }) + } else { + syncBookProgress(book) + } + } + + //自动换源 + if (!book.isLocal && ReadManga.bookSource == null) { + autoChangeSource(book.name, book.author) + return + } + } + + private suspend fun loadChapterListAwait(book: Book): Boolean { + val bookSource = ReadManga.bookSource ?: return true + val oldBook = book.copy() + WebBook.getChapterListAwait(bookSource, book, true).onSuccess { cList -> + if (oldBook.bookUrl == book.bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(oldBook.bookUrl) + appDb.bookChapterDao.insert(*cList.toTypedArray()) + ReadManga.onChapterListUpdated(book) + return true + }.onFailure { + currentCoroutineContext().ensureActive() + //加载章节出错 + ReadManga.mCallback?.loadFail(appCtx.getString(R.string.error_load_toc)) + return false + } + return true + } + + /** + * 加载详情页 + */ + private suspend fun loadBookInfo(book: Book): Boolean { + val source = ReadManga.bookSource ?: return true + try { + WebBook.getBookInfoAwait(source, book, canReName = false) + return true + } catch (e: Throwable) { + currentCoroutineContext().ensureActive() + ReadManga.mCallback?.loadFail("详情页出错: ${e.localizedMessage}") + return false + } + } + + /** + * 自动换源 + */ + private fun autoChangeSource(name: String, author: String) { + if (!AppConfig.autoChangeSource) return + execute { + val sources = appDb.bookSourceDao.allTextEnabledPart + flow { + for (source in sources) { + source.getBookSource()?.let { + emit(it) + } + } + }.onStart { + // 自动换源 + + }.mapParallelSafe(AppConfig.threadCount) { source -> + val book = WebBook.preciseSearchAwait(source, name, author).getOrThrow() + if (book.tocUrl.isEmpty()) { + WebBook.getBookInfoAwait(source, book) + } + val toc = WebBook.getChapterListAwait(source, book).getOrThrow() + val chapter = toc.getOrElse(book.durChapterIndex) { + toc.last() + } + val nextChapter = toc.getOrElse(chapter.index) { + toc.first() + } + WebBook.getContentAwait( + bookSource = source, + book = book, + bookChapter = chapter, + nextChapterUrl = nextChapter.url + ) + book to toc + }.take(1).onEach { (book, toc) -> + changeTo(book, toc) + }.onEmpty { + throw NoStackTraceException("没有合适书源") + }.onCompletion { + // 换源完成 + }.catch { + AppLog.put("自动换源失败\n${it.localizedMessage}", it) + context.toastOnUi("自动换源失败\n${it.localizedMessage}") + }.collect() + } + } + + /** + * 同步进度 + */ + fun syncBookProgress( + book: Book, + alertSync: ((progress: BookProgress) -> Unit)? = null + ) { + if (!AppConfig.syncBookProgress) return + execute { + AppWebDav.getBookProgress(book) + }.onError { + AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it) + }.onSuccess { progress -> + progress ?: return@onSuccess + if (progress.durChapterIndex == book.durChapterIndex && progress.durChapterPos == book.durChapterPos) { + return@onSuccess + } + if (progress.durChapterIndex < book.durChapterIndex || + (progress.durChapterIndex == book.durChapterIndex + && progress.durChapterPos < book.durChapterPos) + ) { + alertSync?.invoke(progress) + } else if (progress.durChapterIndex < book.simulatedTotalChapterNum()) { + ReadManga.setProgress(progress) + AppLog.put("自动同步阅读进度成功《${book.name}》 ${progress.durChapterTitle}") + context.toastOnUi("已同步最新漫画阅读进度") + } + } + } + + /** + * 换源 + */ + fun changeTo(book: Book, toc: List) { + changeSourceCoroutine?.cancel() + changeSourceCoroutine = execute { + //换源中 + ReadManga.book?.migrateTo(book, toc) + book.removeType(BookType.updateError) + ReadManga.book?.delete() + appDb.bookDao.insert(book) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + ReadManga.resetData(book) + ReadManga.loadContent() + }.onError { + AppLog.put("换源失败\n$it", it, true) + }.onFinally { + postEvent(EventBus.SOURCE_CHANGED, book.bookUrl) + } + } + + private fun checkLocalBookFileExist(book: Book): Boolean { + try { + LocalBook.getBookInputStream(book) + return true + } catch (_: Throwable) { + return false + } + } + + fun openChapter(index: Int, durChapterPos: Int = 0) { + if (index < ReadManga.chapterSize) { + ReadManga.showLoading() + ReadManga.durChapterIndex = index + ReadManga.durChapterPos = durChapterPos + ReadManga.saveRead() + ReadManga.loadContent() + } + } + + fun removeFromBookshelf(success: (() -> Unit)?) { + val book = ReadManga.book + Coroutine.async { + book?.delete() + }.onSuccess { + success?.invoke() + } + } + + override fun onCleared() { + super.onCleared() + changeSourceCoroutine?.cancel() + } + + fun refreshContentDur(book: Book) { + execute { + appDb.bookChapterDao.getChapter(book.bookUrl, ReadManga.durChapterIndex) + ?.let { chapter -> + BookHelp.delContent(book, chapter) + openChapter(ReadManga.durChapterIndex, ReadManga.durChapterPos) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterConfig.kt b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterConfig.kt new file mode 100755 index 000000000..a1a8a8388 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterConfig.kt @@ -0,0 +1,18 @@ +package io.legado.app.ui.book.manga.config + +import io.legado.app.utils.GSON + +data class MangaColorFilterConfig( + var r: Int = 0, + var g: Int = 0, + var b: Int = 0, + var a: Int = 0, + var l: Int = 0 +) { + fun toJson(): String { + if (r == 0 && g == 0 && b == 0 && a == 0 && l == 0) { + return "" + } + return GSON.toJson(this) + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterDialog.kt b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterDialog.kt new file mode 100755 index 000000000..92e0680cb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaColorFilterDialog.kt @@ -0,0 +1,79 @@ +package io.legado.app.ui.book.manga.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogMangaColorFilterBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class MangaColorFilterDialog : BaseDialogFragment(R.layout.dialog_manga_color_filter) { + private val binding by viewBinding(DialogMangaColorFilterBinding::bind) + private val mConfig = + GSON.fromJsonObject(AppConfig.mangaColorFilter).getOrNull() + ?: MangaColorFilterConfig() + private val callback get() = activity as? Callback + + override fun onStart() { + super.onStart() + dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initData() + initView() + } + + private fun initData() { + binding.run { + dsbBrightness.progress = mConfig.l + dsbR.progress = mConfig.r + dsbG.progress = mConfig.g + dsbB.progress = mConfig.b + dsbA.progress = mConfig.a + } + } + + private fun initView() { + binding.run { + dsbBrightness.onChanged = { + mConfig.l = it + callback?.updateColorFilter(mConfig) + } + dsbR.onChanged = { + mConfig.r = it + callback?.updateColorFilter(mConfig) + } + dsbG.onChanged = { + mConfig.g = it + callback?.updateColorFilter(mConfig) + } + dsbB.onChanged = { + mConfig.b = it + callback?.updateColorFilter(mConfig) + } + dsbA.onChanged = { + mConfig.a = it + callback?.updateColorFilter(mConfig) + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + AppConfig.mangaColorFilter = mConfig.toJson() + } + + interface Callback { + fun updateColorFilter(config: MangaColorFilterConfig) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/config/MangaEpaperDialog.kt b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaEpaperDialog.kt new file mode 100755 index 000000000..1df27d8c1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaEpaperDialog.kt @@ -0,0 +1,50 @@ +package io.legado.app.ui.book.manga.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogMangaEpaperBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class MangaEpaperDialog : BaseDialogFragment(R.layout.dialog_manga_epaper) { + private val binding by viewBinding(DialogMangaEpaperBinding::bind) + private val callback get() = activity as? Callback + private var mMangaEInkThreshold = 150 + override fun onStart() { + super.onStart() + dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initData() + initView() + } + + private fun initData() { + binding.dsbEpaper.progress = AppConfig.mangaEInkThreshold + } + + private fun initView() { + binding.dsbEpaper.onChanged = { + mMangaEInkThreshold = it + callback?.updateEepaper(it) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + AppConfig.mangaEInkThreshold = mMangaEInkThreshold + } + + interface Callback { + fun updateEepaper(value: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterConfig.kt b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterConfig.kt new file mode 100755 index 000000000..eef7771e2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterConfig.kt @@ -0,0 +1,17 @@ +package io.legado.app.ui.book.manga.config + +import androidx.annotation.Keep +import io.legado.app.ui.widget.ReaderInfoBarView + +@Keep +data class MangaFooterConfig( + var hideChapterLabel: Boolean = false, + var hideChapter: Boolean = false, + var hidePageNumberLabel: Boolean = false, + var hidePageNumber: Boolean = false, + var hideProgressRatioLabel: Boolean = false, + var hideProgressRatio: Boolean = false, + var footerOrientation: Int = ReaderInfoBarView.ALIGN_LEFT,//默认靠左 + var hideFooter: Boolean = false, + var hideChapterName:Boolean=false, +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterSettingDialog.kt b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterSettingDialog.kt new file mode 100755 index 000000000..348161b98 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/config/MangaFooterSettingDialog.kt @@ -0,0 +1,112 @@ +package io.legado.app.ui.book.manga.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogMangaFooterSettingBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.ui.widget.ReaderInfoBarView +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.postEvent +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class MangaFooterSettingDialog : BaseDialogFragment(R.layout.dialog_manga_footer_setting) { + val config = GSON.fromJsonObject(AppConfig.mangaFooterConfig).getOrNull() + ?: MangaFooterConfig() + private val binding by viewBinding(DialogMangaFooterSettingBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.cbChapterLabel.run { + isChecked = config.hideChapterLabel + setOnCheckedChangeListener { _, isChecked -> + config.hideChapterLabel = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.cbChapter.run { + isChecked = config.hideChapter + setOnCheckedChangeListener { _, isChecked -> + config.hideChapter = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.cbPageNumberLabel.run { + isChecked = config.hidePageNumberLabel + setOnCheckedChangeListener { _, isChecked -> + config.hidePageNumberLabel = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.cbPageNumber.run { + isChecked = config.hidePageNumber + setOnCheckedChangeListener { _, isChecked -> + config.hidePageNumber = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.cbProgressRatioLabel.run { + isChecked = config.hideProgressRatioLabel + setOnCheckedChangeListener { _, isChecked -> + config.hideProgressRatioLabel = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.cbProgressRatio.run { + isChecked = config.hideProgressRatio + setOnCheckedChangeListener { _, isChecked -> + config.hideProgressRatio = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.cbChapterName.run { + isChecked = config.hideChapterName + setOnCheckedChangeListener { _, isChecked -> + config.hideChapterName = isChecked + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + binding.rgFooterOrientation.check(if (config.footerOrientation == ReaderInfoBarView.ALIGN_CENTER) R.id.rb_center else R.id.rb_left) + binding.rgFooterOrientation.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.rb_left -> { + config.footerOrientation = ReaderInfoBarView.ALIGN_LEFT + } + + R.id.rb_center -> { + config.footerOrientation = ReaderInfoBarView.ALIGN_CENTER + } + } + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + + binding.rgFooter.check(if (config.hideFooter) R.id.rb_hide else R.id.rb_show) + binding.rgFooter.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.rb_show -> { + config.hideFooter = false + } + + R.id.rb_hide -> { + config.hideFooter = true + } + } + postEvent(EventBus.UP_MANGA_CONFIG, config) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + AppConfig.mangaFooterConfig = GSON.toJson(config) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/BaseMangaPage.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/BaseMangaPage.kt new file mode 100755 index 000000000..60bac9109 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/BaseMangaPage.kt @@ -0,0 +1,6 @@ +package io.legado.app.ui.book.manga.entities + +interface BaseMangaPage { + val chapterIndex: Int + val index: Int +} diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/EpaperTransformation.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/EpaperTransformation.kt new file mode 100755 index 000000000..dbba916d2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/EpaperTransformation.kt @@ -0,0 +1,80 @@ +package io.legado.app.ui.book.manga.entities + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import androidx.annotation.IntRange +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.security.MessageDigest + +/** + * 墨水屏图片转换器。 + * 将彩色图片转换为灰度图,并可选择进行简单的二值化处理,以提高墨水屏显示效果。 + * + * @param threshold 二值化的阈值(0-255)。低于此值的像素变为黑色,高于此值的像素变为白色。 + * 仅当applyBinarization为true时有效。 + */ +class EpaperTransformation( + @param:IntRange(0, 255) private val threshold: Int = 128, +) : BitmapTransformation() { + + private val ID = + "io.legado.app.model.EpaperTransformation.${threshold}" + private val ID_BYTES = ID.toByteArray(CHARSET) + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int, + ): Bitmap { + val resultBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(resultBitmap) + val paint = Paint() + + val colorMatrix = ColorMatrix() + colorMatrix.setSaturation(0f) + val filter = ColorMatrixColorFilter(colorMatrix) + paint.colorFilter = filter + canvas.drawBitmap(toTransform, 0f, 0f, paint) + + val pixels = IntArray(outWidth * outHeight) + resultBitmap.getPixels(pixels, 0, outWidth, 0, 0, outWidth, outHeight) + + for (i in pixels.indices) { + val pixel = pixels[i] + val gray = Color.red(pixel) + pixels[i] = + if (gray < threshold) Color.BLACK else Color.WHITE + } + resultBitmap.setPixels(pixels, 0, outWidth, 0, 0, outWidth, outHeight) + + return resultBitmap + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EpaperTransformation + + if (threshold != other.threshold) return false + if (ID != other.ID) return false + + return true + } + + override fun hashCode(): Int { + var result = 31 + threshold + result = 31 * result + ID.hashCode() + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/GrayscaleTransformation.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/GrayscaleTransformation.kt new file mode 100755 index 000000000..a3533dcfd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/GrayscaleTransformation.kt @@ -0,0 +1,57 @@ +package io.legado.app.ui.book.manga.entities + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +class GrayscaleTransformation : BitmapTransformation() { + private val ID = "io.legado.app.model.GrayscaleTransformation" + + private val ID_BYTES = ID.toByteArray(StandardCharsets.UTF_8) + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int, + ): Bitmap { + val resultBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) + + val canvas = Canvas(resultBitmap) + val paint = Paint() + + val matrix = ColorMatrix( + floatArrayOf( + 0.299f, 0.587f, 0.114f, 0f, 0f, + 0.299f, 0.587f, 0.114f, 0f, 0f, + 0.299f, 0.587f, 0.114f, 0f, 0f, + 0f, 0f, 0f, 1f, 0f + ) + ) + val filter = ColorMatrixColorFilter(matrix) + paint.colorFilter = filter + canvas.drawBitmap(toTransform, 0f, 0f, paint) + return resultBitmap + } + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as GrayscaleTransformation + return ID == other.ID + } + + override fun hashCode(): Int { + return ID.hashCode() + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaChapter.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaChapter.kt new file mode 100755 index 000000000..93b2acdd0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaChapter.kt @@ -0,0 +1,9 @@ +package io.legado.app.ui.book.manga.entities + +import io.legado.app.data.entities.BookChapter + +data class MangaChapter( + val chapter: BookChapter, + val pages: List, + val imageCount: Int +) diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaContent.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaContent.kt new file mode 100755 index 000000000..904297882 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaContent.kt @@ -0,0 +1,8 @@ +package io.legado.app.ui.book.manga.entities + +data class MangaContent( + val pos: Int, + val items: List, + val curFinish: Boolean, + val nextFinish: Boolean +) diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaPage.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaPage.kt new file mode 100755 index 000000000..99ecb4768 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/MangaPage.kt @@ -0,0 +1,10 @@ +package io.legado.app.ui.book.manga.entities + +data class MangaPage( + override val chapterIndex: Int = 0,//总章节位置 + val chapterSize: Int,//总章节数量 + val mImageUrl: String = "",//当前URL + override val index: Int = 0,//当前章节位置 + var imageCount: Int = 0,//当前章节内容总数 + val mChapterName: String = "",//章节名称 +) : BaseMangaPage diff --git a/app/src/main/java/io/legado/app/ui/book/manga/entities/ReaderLoading.kt b/app/src/main/java/io/legado/app/ui/book/manga/entities/ReaderLoading.kt new file mode 100755 index 000000000..58e17bb09 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/entities/ReaderLoading.kt @@ -0,0 +1,9 @@ +package io.legado.app.ui.book.manga.entities + +data class ReaderLoading( + override val chapterIndex: Int = 0, + override val index: Int = 0, + val mMessage: String? = null, + val isVolume: Boolean = false +) : BaseMangaPage + diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/GestureDetectorWithLongTap.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/GestureDetectorWithLongTap.kt new file mode 100755 index 000000000..8d07390dc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/GestureDetectorWithLongTap.kt @@ -0,0 +1,64 @@ +package io.legado.app.ui.book.manga.recyclerview + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ViewConfiguration +import kotlin.math.abs + +open class GestureDetectorWithLongTap( + context: Context, + listener: Listener, +) : GestureDetector(context, listener) { + + private val handler = Handler(Looper.getMainLooper()) + private val slop = ViewConfiguration.get(context).scaledTouchSlop + private val longTapTime = ViewConfiguration.getLongPressTimeout().toLong() + private val doubleTapTime = ViewConfiguration.getDoubleTapTimeout().toLong() + + private var downX = 0f + private var downY = 0f + private var lastUp = 0L + private var lastDownEvent: MotionEvent? = null + + + private val longTapFn = Runnable { listener.onLongTapConfirmed(lastDownEvent!!) } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + lastDownEvent?.recycle() + lastDownEvent = MotionEvent.obtain(ev) + + if (ev.downTime - lastUp > doubleTapTime) { + downX = ev.rawX + downY = ev.rawY + handler.postDelayed(longTapFn, longTapTime) + } + } + + MotionEvent.ACTION_MOVE -> { + if (abs(ev.rawX - downX) > slop || abs(ev.rawY - downY) > slop) { + handler.removeCallbacks(longTapFn) + } + } + + MotionEvent.ACTION_UP -> { + lastUp = ev.eventTime + handler.removeCallbacks(longTapFn) + } + + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> { + handler.removeCallbacks(longTapFn) + } + } + return super.onTouchEvent(ev) + } + + open class Listener : SimpleOnGestureListener() { + open fun onLongTapConfirmed(ev: MotionEvent) { + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaAdapter.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaAdapter.kt new file mode 100755 index 000000000..08abae4c7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaAdapter.kt @@ -0,0 +1,280 @@ +package io.legado.app.ui.book.manga.recyclerview + +import android.content.Context +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.util.SparseArray +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import androidx.annotation.IntRange +import androidx.core.util.size +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.bumptech.glide.Glide +import com.bumptech.glide.ListPreloader.PreloadModelProvider +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter.Companion.TYPE_FOOTER_VIEW +import io.legado.app.databinding.ItemBookMangaEdgeBinding +import io.legado.app.databinding.ItemBookMangaPageBinding +import io.legado.app.help.glide.progress.ProgressManager +import io.legado.app.model.BookCover +import io.legado.app.model.ReadManga +import io.legado.app.ui.book.manga.config.MangaColorFilterConfig +import io.legado.app.ui.book.manga.entities.EpaperTransformation +import io.legado.app.ui.book.manga.entities.GrayscaleTransformation +import io.legado.app.ui.book.manga.entities.MangaPage +import io.legado.app.ui.book.manga.entities.ReaderLoading +import io.legado.app.utils.dpToPx + + +class MangaAdapter(private val context: Context) : + RecyclerView.Adapter(), PreloadModelProvider { + + private val inflater: LayoutInflater = LayoutInflater.from(context) + private lateinit var mConfig: MangaColorFilterConfig + private var mTransformation: BitmapTransformation? = null + private var currentMangaEInkThreshold = 0 + + companion object { + private const val LOADING_VIEW = 0 + private const val CONTENT_VIEW = 1 + } + + var isHorizontal = false + + private val mDiffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean { + return if (oldItem is ReaderLoading && newItem is ReaderLoading) { + newItem.mMessage == oldItem.mMessage + } else if (oldItem is MangaPage && newItem is MangaPage) { + oldItem.mImageUrl == newItem.mImageUrl + } else false + } + + override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean { + return if (oldItem is ReaderLoading && newItem is ReaderLoading) { + oldItem == newItem + } else if (oldItem is MangaPage && newItem is MangaPage) { + oldItem == newItem + } else false + } + } + + private val mDiffer = AsyncListDiffer(this, mDiffCallback) + + fun getItem(@IntRange(from = 0) position: Int) = mDiffer.currentList.getOrNull(position) + + fun getItems() = mDiffer.currentList + + fun isEmpty() = mDiffer.currentList.isEmpty() + + fun isNotEmpty() = !isEmpty() + + //全部替换数据 + fun submitList(contents: List, runnable: Runnable? = null) { + mDiffer.submitList(contents, runnable) + } + + inner class PageViewHolder(binding: ItemBookMangaPageBinding) : + MangaVH(binding, context) { + + init { + initComponent( + binding.loading, + binding.image, + binding.progress, + binding.retry, + binding.flProgress + ) + binding.retry.setOnClickListener { + val item = mDiffer.currentList[layoutPosition] + if (item is MangaPage) { + val isLastImage = item.imageCount > 0 && item.index == item.imageCount - 1 + loadImageWithRetry( + item.mImageUrl, isHorizontal, isLastImage, mTransformation + ) + } + } + } + + fun onBind(item: MangaPage) { + setImageColorFilter() + val isLastImage = item.imageCount > 0 && item.index == item.imageCount - 1 + loadImageWithRetry(item.mImageUrl, isHorizontal, isLastImage, mTransformation) + } + + fun setImageColorFilter() { + require( + mConfig.r in 0..255 && + mConfig.g in 0..255 && + mConfig.b in 0..255 && + mConfig.a in 0..255 + ) { + "ARGB values must be between 0-255" + } + val matrix = floatArrayOf( + (255 - mConfig.r) / 255f, 0f, 0f, 0f, 0f, + 0f, (255 - mConfig.g) / 255f, 0f, 0f, 0f, + 0f, 0f, (255 - mConfig.b) / 255f, 0f, 0f, + 0f, 0f, 0f, (255 - mConfig.a) / 255f, 0f + ) + binding.image.colorFilter = ColorMatrixColorFilter(ColorMatrix(matrix)) + } + } + + inner class PageMoreViewHolder(val binding: ItemBookMangaEdgeBinding) : + RecyclerView.ViewHolder(binding.root) { + fun onBind(item: ReaderLoading) { + val message = item.mMessage + binding.text.text = message + itemView.updateLayoutParams { + height = if (item.isVolume) { + MATCH_PARENT + } else { + 96.dpToPx() + } + } + } + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when { + viewType >= TYPE_FOOTER_VIEW -> { + ItemViewHolder(footerItems.get(viewType).invoke(parent)) + } + + viewType == LOADING_VIEW -> { + PageMoreViewHolder(ItemBookMangaEdgeBinding.inflate(inflater, parent, false)) + } + + viewType == CONTENT_VIEW -> { + PageViewHolder(ItemBookMangaPageBinding.inflate(inflater, parent, false)) + } + + else -> error("Unknown view type!") + } + } + + override fun getItemCount(): Int = getActualItemCount() + getFooterCount() + + override fun getItemViewType(position: Int): Int { + return when { + isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() + getItem(position) is MangaPage -> CONTENT_VIEW + getItem(position) is ReaderLoading -> LOADING_VIEW + else -> error("Unknown view type!") + } + } + + fun getFooterCount() = footerItems.size + + private fun isFooter(position: Int) = position >= getActualItemCount() + + override fun onViewRecycled(vh: RecyclerView.ViewHolder) { + super.onViewRecycled(vh) + when (vh) { + is PageViewHolder -> { + vh.itemView.updateLayoutParams { + height = MATCH_PARENT + } + Glide.with(context).clear(vh.binding.image) + if (vh.binding.image.tag is String) { + ProgressManager.removeListener(vh.binding.image.tag as String) + } + } + } + } + + override fun onBindViewHolder(vh: RecyclerView.ViewHolder, position: Int) { + when (vh) { + is PageViewHolder -> vh.onBind(getItem(position) as MangaPage) + is PageMoreViewHolder -> vh.onBind(getItem(position) as ReaderLoading) + } + } + + + private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() } + + @Synchronized + fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = getActualItemCount() + footerItems.size + footerItems.put(TYPE_FOOTER_VIEW + footerItems.size, footer) + notifyItemInserted(index) + } + } + + /** + * 除去header和footer + */ + fun getActualItemCount() = getItems().size + + @Synchronized + fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) { + kotlin.runCatching { + val index = footerItems.indexOfValue(footer) + if (index >= 0) { + footerItems.remove(index) + notifyItemRemoved(getActualItemCount() + index - 2) + } + } + } + + override fun getPreloadItems(position: Int): List { + if (isEmpty() || position >= getItems().size) { + return emptyList() + } + return getItems().subList(position, position + 1) + } + + override fun getPreloadRequestBuilder(item: Any): RequestBuilder<*>? { + if (item is MangaPage) { + return BookCover.preloadManga( + context, + item.mImageUrl, + sourceOrigin = ReadManga.book?.origin, + ) + } + return null + } + + fun setMangaImageColorFilter(config: MangaColorFilterConfig) { + mConfig = config + notifyItemRangeChanged(0, itemCount) + } + + fun enableMangaEInk(enable: Boolean, value: Int) { + if (enable) { + currentMangaEInkThreshold = value + mTransformation = EpaperTransformation(currentMangaEInkThreshold) + } else { + mTransformation = null + } + notifyItemRangeChanged(0, itemCount) + } + + fun updateThreshold(mangaEInkThreshold: Int) { + if (currentMangaEInkThreshold != mangaEInkThreshold) { + currentMangaEInkThreshold = mangaEInkThreshold + mTransformation = EpaperTransformation(currentMangaEInkThreshold) + notifyItemRangeChanged(0, itemCount) + } + } + + //开启灰色图片 + fun enableGray(enable: Boolean) { + mTransformation = if (enable) { + GrayscaleTransformation() + } else { + null + } + notifyItemRangeChanged(0, itemCount) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaLayoutManager.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaLayoutManager.kt new file mode 100755 index 000000000..d75b220d0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaLayoutManager.kt @@ -0,0 +1,17 @@ +package io.legado.app.ui.book.manga.recyclerview + +import android.content.Context +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class MangaLayoutManager(context: Context) : + LinearLayoutManager(context) { + + private val extraLayoutSpace = context.resources.displayMetrics.heightPixels * 3 / 4 + + @Deprecated("Deprecated in Java") + override fun getExtraLayoutSpace(state: RecyclerView.State?): Int { + return extraLayoutSpace + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaVH.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaVH.kt new file mode 100755 index 000000000..2c68d28b7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/MangaVH.kt @@ -0,0 +1,141 @@ +package io.legado.app.ui.book.manga.recyclerview + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.view.Gravity +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import io.legado.app.help.glide.progress.ProgressManager +import io.legado.app.model.BookCover +import io.legado.app.model.ReadManga +import io.legado.app.utils.printOnDebug + +open class MangaVH(val binding: VB, private val context: Context) : + RecyclerView.ViewHolder(binding.root) { + + protected lateinit var mLoading: ProgressBar + protected lateinit var mImage: AppCompatImageView + protected lateinit var mProgress: TextView + protected lateinit var mFlProgress: FrameLayout + protected var mRetry: Button? = null + + private val minHeight = context.resources.displayMetrics.heightPixels * 2 / 3 + + fun initComponent( + loading: ProgressBar, + image: AppCompatImageView, + progress: TextView, + button: Button? = null, + flProgress: FrameLayout, + ) { + mLoading = loading + mImage = image + mRetry = button + mProgress = progress + mFlProgress = flProgress + } + + @SuppressLint("CheckResult") + fun loadImageWithRetry( + imageUrl: String, + isHorizontal: Boolean, + isLastImage: Boolean, + transformation: Transformation? + ) { + mFlProgress.isVisible = true + mLoading.isVisible = true + mRetry?.isGone = true + mProgress.isVisible = true + ProgressManager.removeListener(imageUrl) + ProgressManager.addListener(imageUrl) { _, percentage, _, _ -> + @SuppressLint("SetTextI18n") + mProgress.text = "$percentage%" + } + try { + mImage.tag = imageUrl + BookCover.loadManga( + context, + imageUrl, + sourceOrigin = ReadManga.book?.origin, + transformation = transformation + ).addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean, + ): Boolean { + mFlProgress.isVisible = true + mLoading.isGone = true + mRetry?.isVisible = true + mProgress.isGone = true + itemView.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean, + ): Boolean { + mFlProgress.isGone = true + if (!isHorizontal) { + itemView.updateLayoutParams { + height = ViewGroup.LayoutParams.WRAP_CONTENT + } + mImage.updateLayoutParams { + gravity = Gravity.NO_GRAVITY + } + if (isLastImage) { + mImage.updateLayoutParams { + height = ViewGroup.LayoutParams.WRAP_CONTENT + } + itemView.minimumHeight = minHeight + } else { + mImage.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + itemView.minimumHeight = 0 + } + mImage.scaleType = ImageView.ScaleType.FIT_XY + } else { + itemView.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + itemView.minimumHeight = 0 + mImage.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + gravity = Gravity.CENTER + } + mImage.scaleType = ImageView.ScaleType.FIT_CENTER + } + return false + } + }).into(mImage) + } catch (e: Exception) { + e.printOnDebug() + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/ScrollTimer.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/ScrollTimer.kt new file mode 100755 index 000000000..fb46d3523 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/ScrollTimer.kt @@ -0,0 +1,75 @@ +package io.legado.app.ui.book.manga.recyclerview + +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +class ScrollTimer( + private val callback: ScrollCallback, + private val recyclerView: RecyclerView, + private val coroutineScope: CoroutineScope, +) : RecyclerView.OnScrollListener() { + private var distance = 1 + private var mScrollPageJob: Job? = null + var isEnabled: Boolean = false + set(value) { + if (field != value) { + field = value + if (value) { + recyclerView.addOnScrollListener(this) + startScroll() + } else { + recyclerView.removeOnScrollListener(this) + recyclerView.stopScroll() + } + } + } + + var isEnabledPage: Boolean = false + set(value) { + if (field != value) { + field = value + if (value) { + mScrollPageJob?.cancel() + startScrollPage() + } else { + mScrollPageJob?.cancel() + } + } + } + + override fun onScrollStateChanged( + recyclerView: RecyclerView, + newState: Int, + ) { + if (newState == SCROLL_STATE_IDLE) { + startScroll() + } + } + + fun setSpeed(distance: Int) { + this.distance = distance + } + + private fun startScroll() { + callback.scrollBy(distance) + } + + private fun startScrollPage() { + mScrollPageJob = coroutineScope.launch { + while (isActive) { + delay(distance.times(1000L)) + callback.scrollPage() + } + } + } + + interface ScrollCallback { + fun scrollBy(distance: Int) + fun scrollPage() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonFrame.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonFrame.kt new file mode 100755 index 000000000..4284a5a23 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonFrame.kt @@ -0,0 +1,126 @@ +package io.legado.app.ui.book.manga.recyclerview + +import android.content.Context +import android.graphics.Rect +import android.graphics.RectF +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.widget.FrameLayout + +class WebtoonFrame : FrameLayout { + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( + context, + attrs, + defStyle + ) + + private val scaleDetector = ScaleGestureDetector(context, ScaleListener()) + private val flingDetector = GestureDetector(context, FlingListener()) + + var doubleTapZoom = true + set(value) { + field = value + recycler?.doubleTapZoom = value + scaleDetector.isQuickScaleEnabled = value + } + + var disableMangaScale = false + + private val recycler: WebtoonRecyclerView? + get() = getChildAt(0) as? WebtoonRecyclerView + + private val mcRect = RectF() + private val blRect = RectF() + private val brRect = RectF() + + private var mTouchMiddle: (() -> Unit)? = null + fun onTouchMiddle(init: () -> Unit) = apply { this.mTouchMiddle = init } + private var mNextPage: (() -> Unit)? = null + fun onNextPage(init: () -> Unit) = apply { this.mNextPage = init } + private var mPrevPage: (() -> Unit)? = null + fun onPrevPage(init: () -> Unit) = apply { this.mPrevPage = init } + + var disabledClickScroll = false + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + recycler?.tapListener = { ev -> + when { + mcRect.contains(ev.rawX, ev.rawY) -> { + mTouchMiddle?.invoke() + } + + blRect.contains(ev.rawX, ev.rawY) && !disabledClickScroll -> { + mPrevPage?.invoke() + } + + brRect.contains(ev.rawX, ev.rawY) && !disabledClickScroll -> { + mNextPage?.invoke() + } + } + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mcRect.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) + blRect.set(0f, height * 0.66f, width * 0.33f, height.toFloat()) + brRect.set(width * 0.66f, height * 0.66f, width.toFloat(), height.toFloat()) + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (!disableMangaScale) { + scaleDetector.onTouchEvent(ev) + flingDetector.onTouchEvent(ev) + val recyclerRect = Rect() + recycler?.getHitRect(recyclerRect) ?: return super.dispatchTouchEvent(ev) + recyclerRect.inset(1, 1) + + if (recyclerRect.right < recyclerRect.left || recyclerRect.bottom < recyclerRect.top) { + return super.dispatchTouchEvent(ev) + } + + ev.setLocation( + ev.x.coerceIn(recyclerRect.left.toFloat(), recyclerRect.right.toFloat()), + ev.y.coerceIn(recyclerRect.top.toFloat(), recyclerRect.bottom.toFloat()), + ) + } + return super.dispatchTouchEvent(ev) + } + + inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + recycler?.onScaleBegin() + return true + } + + override fun onScale(detector: ScaleGestureDetector): Boolean { + recycler?.onScale(detector.scaleFactor) + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) { + recycler?.onScaleEnd() + } + } + + inner class FlingListener : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float, + ): Boolean { + return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonRecyclerView.kt b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonRecyclerView.kt new file mode 100755 index 000000000..fa83e9d79 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/manga/recyclerview/WebtoonRecyclerView.kt @@ -0,0 +1,373 @@ +package io.legado.app.ui.book.manga.recyclerview + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.HapticFeedbackConstants +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.view.animation.DecelerateInterpolator +import androidx.core.animation.doOnEnd +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.utils.findCenterViewPosition +import kotlin.math.abs + + +class WebtoonRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : RecyclerView(context, attrs, defStyle) { + + private var isZooming = false + private var atLastPosition = false + private var atFirstPosition = false + private var halfWidth = 0 + private var halfHeight = 0 + private var originalHeight = 0 + private var heightSet = false + private var firstVisibleItemPosition = 0 + private var lastVisibleItemPosition = 0 + private var currentScale = DEFAULT_RATE + private var mLastCenterViewPosition = 0 + + private var mPreScrollListener: IComicPreScroll? = null + private var mNestedPreScrollListener: IComicPreScroll? = null + private val listener = GestureListener() + private val detector = Detector() + + var doubleTapZoom = true + var tapListener: ((MotionEvent) -> Unit)? = null + var longTapListener: ((MotionEvent) -> Boolean)? = null + var disableMangaScale = false + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + halfWidth = MeasureSpec.getSize(widthSpec) / 2 + halfHeight = MeasureSpec.getSize(heightSpec) / 2 + if (!heightSet) { + originalHeight = MeasureSpec.getSize(heightSpec) + heightSet = true + } + super.onMeasure(widthSpec, heightSpec) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(e: MotionEvent): Boolean { + return detector.onTouchEvent(e) || super.onTouchEvent(e) + } + + override fun onScrolled(dx: Int, dy: Int) { + super.onScrolled(dx, dy) + val layoutManager = layoutManager as LinearLayoutManager + lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + + val position = findCenterViewPosition() + if (position != NO_POSITION && position != mLastCenterViewPosition) { + mLastCenterViewPosition = position + mPreScrollListener?.onPreScrollListener(this, dx, dy, position) + } + } + + override fun onScrollStateChanged(state: Int) { + super.onScrollStateChanged(state) + val layoutManager = layoutManager + val visibleItemCount = layoutManager?.childCount ?: 0 + val totalItemCount = layoutManager?.itemCount ?: 0 + atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1 + atFirstPosition = firstVisibleItemPosition == 0 + } + + override fun dispatchNestedPreScroll( + dx: Int, + dy: Int, + consumed: IntArray?, + offsetInWindow: IntArray?, + type: Int + ): Boolean { + val position = findCenterViewPosition() + mNestedPreScrollListener?.onPreScrollListener(this, dx, dy, position) + return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type) + } + + private fun getPositionX(positionX: Float): Float { + if (currentScale < 1) { + return 0f + } + val maxPositionX = halfWidth * (currentScale - 1) + return positionX.coerceIn(-maxPositionX, maxPositionX) + } + + private fun getPositionY(positionY: Float): Float { + if (currentScale < 1) { + return (originalHeight / 2 - halfHeight).toFloat() + } + val maxPositionY = halfHeight * (currentScale - 1) + return positionY.coerceIn(-maxPositionY, maxPositionY) + } + + private fun zoom( + fromRate: Float, + toRate: Float, + fromX: Float, + toX: Float, + fromY: Float, + toY: Float, + ) { + isZooming = true + val animatorSet = AnimatorSet() + val translationXAnimator = ValueAnimator.ofFloat(fromX, toX) + translationXAnimator.addUpdateListener { animation -> x = animation.animatedValue as Float } + + val translationYAnimator = ValueAnimator.ofFloat(fromY, toY) + translationYAnimator.addUpdateListener { animation -> y = animation.animatedValue as Float } + + val scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate) + scaleAnimator.addUpdateListener { animation -> + currentScale = animation.animatedValue as Float + setScaleRate(currentScale) + } + animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator) + animatorSet.duration = ANIMATOR_DURATION_TIME.toLong() + animatorSet.interpolator = DecelerateInterpolator() + animatorSet.start() + animatorSet.doOnEnd { + isZooming = false + currentScale = toRate + } + } + + fun zoomFling(velocityX: Int, velocityY: Int): Boolean { + if (currentScale <= 1f) return false + + val distanceTimeFactor = 0.4f + val animatorSet = AnimatorSet() + + if (velocityX != 0) { + val dx = (distanceTimeFactor * velocityX / 2) + val newX = getPositionX(x + dx) + val translationXAnimator = ValueAnimator.ofFloat(x, newX) + translationXAnimator.addUpdateListener { animation -> + x = getPositionX(animation.animatedValue as Float) + } + animatorSet.play(translationXAnimator) + } + if (velocityY != 0 && (atFirstPosition || atLastPosition)) { + val dy = (distanceTimeFactor * velocityY / 2) + val newY = getPositionY(y + dy) + val translationYAnimator = ValueAnimator.ofFloat(y, newY) + translationYAnimator.addUpdateListener { animation -> + y = getPositionY(animation.animatedValue as Float) + } + animatorSet.play(translationYAnimator) + } + + animatorSet.duration = 400 + animatorSet.interpolator = DecelerateInterpolator() + animatorSet.start() + + return true + } + + fun resetZoom() { + zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) + } + + private fun zoomScrollBy(dx: Int, dy: Int) { + if (dx != 0) { + x = getPositionX(x + dx) + } + if (dy != 0) { + y = getPositionY(y + dy) + } + } + + private fun setScaleRate(rate: Float) { + scaleX = rate + scaleY = rate + } + + fun onScale(scaleFactor: Float) { + currentScale *= scaleFactor + currentScale = currentScale.coerceIn( + MIN_RATE, + MAX_SCALE_RATE, + ) + + setScaleRate(currentScale) + + layoutParams.height = if (currentScale < 1) { + (originalHeight / currentScale).toInt() + } else { + originalHeight + } + halfHeight = layoutParams.height / 2 + + if (currentScale != DEFAULT_RATE) { + x = getPositionX(x) + y = getPositionY(y) + } else { + x = 0f + y = 0f + } + + requestLayout() + } + + fun onScaleBegin() { + if (detector.isDoubleTapping) { + detector.isQuickScaling = true + } + } + + fun onScaleEnd() { + if (scaleX < MIN_RATE) { + zoom(currentScale, MIN_RATE, x, 0f, y, 0f) + } + } + + inner class GestureListener : GestureDetectorWithLongTap.Listener() { + + override fun onSingleTapConfirmed(ev: MotionEvent): Boolean { + tapListener?.invoke(ev) + return false + } + + override fun onDoubleTap(ev: MotionEvent): Boolean { + detector.isDoubleTapping = true + return false + } + + fun onDoubleTapConfirmed(ev: MotionEvent) { + if (!isZooming && doubleTapZoom) { + if (scaleX != DEFAULT_RATE) { + zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f) + } else { + val toScale = 2f + val toX = (halfWidth - ev.x) * (toScale - 1) + val toY = (halfHeight - ev.y) * (toScale - 1) + zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY) + } + } + } + + override fun onLongTapConfirmed(ev: MotionEvent) { + if (longTapListener?.invoke(ev) == true) { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + } + } + } + + inner class Detector : GestureDetectorWithLongTap(context, listener) { + + private var scrollPointerId = 0 + private var downX = 0 + private var downY = 0 + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + private var isZoomDragging = false + var isDoubleTapping = false + var isQuickScaling = false + override fun onTouchEvent(ev: MotionEvent): Boolean { + val action = ev.actionMasked + val actionIndex = ev.actionIndex + + when (action) { + MotionEvent.ACTION_DOWN -> { + scrollPointerId = ev.getPointerId(0) + downX = (ev.x + 0.5f).toInt() + downY = (ev.y + 0.5f).toInt() + } + + MotionEvent.ACTION_POINTER_DOWN -> { + scrollPointerId = ev.getPointerId(actionIndex) + downX = (ev.getX(actionIndex) + 0.5f).toInt() + downY = (ev.getY(actionIndex) + 0.5f).toInt() + } + + MotionEvent.ACTION_MOVE -> { + if (disableMangaScale) { + return super.onTouchEvent(ev) + } + if (isDoubleTapping && isQuickScaling) { + return true + } + + val index = ev.findPointerIndex(scrollPointerId) + if (index < 0) { + return false + } + + val x = (ev.getX(index) + 0.5f).toInt() + val y = (ev.getY(index) + 0.5f).toInt() + var dx = x - downX + var dy = if (atFirstPosition || atLastPosition) y - downY else 0 + + if (!isZoomDragging && currentScale > 1f) { + var startScroll = false + + if (abs(dx) > touchSlop) { + if (dx < 0) { + dx += touchSlop + } else { + dx -= touchSlop + } + startScroll = true + } + if (abs(dy) > touchSlop) { + if (dy < 0) { + dy += touchSlop + } else { + dy -= touchSlop + } + startScroll = true + } + + if (startScroll) { + isZoomDragging = true + } + } + + if (isZoomDragging) { + zoomScrollBy(dx, dy) + } + } + + MotionEvent.ACTION_UP -> { + if (isDoubleTapping && !isQuickScaling && !disableMangaScale) { + listener.onDoubleTapConfirmed(ev) + } + isZoomDragging = false + isDoubleTapping = false + isQuickScaling = false + } + + MotionEvent.ACTION_CANCEL -> { + isZoomDragging = false + isDoubleTapping = false + isQuickScaling = false + } + } + return super.onTouchEvent(ev) + } + } + + fun setPreScrollListener(iComicPreScroll: IComicPreScroll) { + mPreScrollListener = iComicPreScroll + } + + fun setNestedPreScrollListener(iComicPreScroll: IComicPreScroll) { + mNestedPreScrollListener = iComicPreScroll + } + + fun interface IComicPreScroll { + fun onPreScrollListener(recyclerView: RecyclerView, dx: Int, dy: Int, position: Int) + } +} + +private const val ANIMATOR_DURATION_TIME = 200 +private const val MIN_RATE = 0.5f +private const val DEFAULT_RATE = 1f +private const val MAX_SCALE_RATE = 3f diff --git a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt new file mode 100755 index 000000000..173843fd9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt @@ -0,0 +1,392 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.app.DatePickerDialog +import android.content.pm.ActivityInfo +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import android.view.WindowInsets +import android.view.WindowManager +import androidx.activity.viewModels +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst.charsets +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ActivityBookReadBinding +import io.legado.app.databinding.DialogDownloadChoiceBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogSimulatedReadingBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.model.CacheBook +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.config.BgTextConfigDialog +import io.legado.app.ui.book.read.config.ClickActionConfigDialog +import io.legado.app.ui.book.read.config.PaddingConfigDialog +import io.legado.app.ui.book.read.config.PageKeyDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.find +import io.legado.app.utils.getPrefString +import io.legado.app.utils.gone +import io.legado.app.utils.isTv +import io.legado.app.utils.setLightStatusBar +import io.legado.app.utils.setNavigationBarColorAuto +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +/** + * 阅读界面 + */ +abstract class BaseReadBookActivity : + VMBaseActivity(imageBg = false) { + + override val binding by viewBinding(ActivityBookReadBinding::inflate) + override val viewModel by viewModels() + protected val menuLayoutIsVisible + get() = bottomDialog > 0 || binding.readMenu.isVisible || binding.searchMenu.bottomMenuVisible + + var bottomDialog = 0 + set(value) { + if (field != value) { + field = value + onBottomDialogChange() + } + } + private val selectBookFolderResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + ReadBook.book?.let { book -> + FileDoc.fromUri(uri, true).find(book.originName)?.let { doc -> + book.bookUrl = doc.uri.toString() + book.save() + viewModel.loadChapterList(book) + } ?: ReadBook.upMsg("找不到文件") + } + } ?: ReadBook.upMsg("没有权限访问") + } + + override fun onCreate(savedInstanceState: Bundle?) { + ReadBook.msg = null + setOrientation() + upLayoutInDisplayCutoutMode() + super.onCreate(savedInstanceState) + binding.navigationBar.setOnApplyWindowInsetsListenerCompat { view, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.updateLayoutParams { + height = insets.bottom + } + windowInsets + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.navigationBar.setBackgroundColor(bottomBackground) + viewModel.permissionDenialLiveData.observe(this) { + selectBookFolderResult.launch { + mode = HandleFileContract.DIR_SYS + title = "选择书籍所在文件夹" + } + } + if (!LocalConfig.readHelpVersionIsLast) { + if (isTv) { + showCustomPageKeyConfig() + } else { + showClickRegionalConfig() + } + } + } + + private fun onBottomDialogChange() { + when (bottomDialog) { + 0 -> onMenuHide() + 1 -> onMenuShow() + } + } + + open fun onMenuShow() { + + } + + open fun onMenuHide() { + + } + + fun showPaddingConfig() { + showDialogFragment() + } + + fun showBgTextConfig() { + showDialogFragment() + } + + fun showClickRegionalConfig() { + showDialogFragment() + } + + private fun showCustomPageKeyConfig() { + PageKeyDialog(this).show() + } + + /** + * 屏幕方向 + */ + @SuppressLint("SourceLockedOrientationActivity") + fun setOrientation() { + when (AppConfig.screenOrientation) { + "0" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + "1" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + "2" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + "3" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR + "4" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + } + } + + /** + * 更新状态栏,导航栏 + */ + fun upSystemUiVisibility( + isInMultiWindow: Boolean, + toolBarHide: Boolean = true, + useBgMeanColor: Boolean = false + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.insetsController?.run { + if (toolBarHide && ReadBookConfig.hideNavigationBar) { + hide(WindowInsets.Type.navigationBars()) + } else { + show(WindowInsets.Type.navigationBars()) + } + if (toolBarHide && ReadBookConfig.hideStatusBar) { + hide(WindowInsets.Type.statusBars()) + } else { + show(WindowInsets.Type.statusBars()) + } + } + } + upSystemUiVisibilityO(isInMultiWindow, toolBarHide) + if (toolBarHide) { + setLightStatusBar(ReadBookConfig.durConfig.curStatusIconDark()) + } else { + val statusBarColor = + if (AppConfig.readBarStyleFollowPage + && ReadBookConfig.durConfig.curBgType() == 0 + || useBgMeanColor + ) { + ReadBookConfig.bgMeanColor + } else { + ThemeStore.statusBarColor(this, AppConfig.isTransparentStatusBar) + } + setLightStatusBar(ColorUtils.isColorLight(statusBarColor)) + } + } + + @Suppress("DEPRECATION") + private fun upSystemUiVisibilityO( + isInMultiWindow: Boolean, + toolBarHide: Boolean = true + ) { + var flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE + or View.SYSTEM_UI_FLAG_IMMERSIVE + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) + if (!isInMultiWindow) { + flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } + if (ReadBookConfig.hideNavigationBar) { + flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + if (toolBarHide) { + flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + } + } + if (ReadBookConfig.hideStatusBar && toolBarHide) { + flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN + } + window.decorView.systemUiVisibility = flag + } + + override fun upNavigationBarColor() { + upNavigationBar() + when { + binding.readMenu.isVisible -> super.upNavigationBarColor() + binding.searchMenu.bottomMenuVisible -> super.upNavigationBarColor() + bottomDialog > 0 -> super.upNavigationBarColor() + !AppConfig.immNavigationBar -> super.upNavigationBarColor() + else -> setNavigationBarColorAuto(ReadBookConfig.bgMeanColor) + } + } + + @SuppressLint("RtlHardcoded") + private fun upNavigationBar() { + binding.navigationBar.gone(!menuLayoutIsVisible) + } + + /** + * 保持亮屏 + */ + fun keepScreenOn(on: Boolean) { + val isScreenOn = + (window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0 + if (on == isScreenOn) return + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + /** + * 适配刘海 + */ + private fun upLayoutInDisplayCutoutMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.attributes = window.attributes.apply { + layoutInDisplayCutoutMode = if (ReadBookConfig.readBodyToLh) { + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } else { + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + } + } + } + + @SuppressLint("InflateParams", "SetTextI18n") + fun showDownloadDialog() { + ReadBook.book?.let { book -> + alert(titleResource = R.string.offline_cache) { + val alertBinding = DialogDownloadChoiceBinding.inflate(layoutInflater).apply { + editStart.setText((book.durChapterIndex + 1).toString()) + editEnd.setText(book.totalChapterNum.toString()) + } + customView { alertBinding.root } + okButton { + alertBinding.run { + val start = editStart.text!!.toString().let { + if (it.isEmpty()) 0 else it.toInt() + } + val end = editEnd.text!!.toString().let { + if (it.isEmpty()) book.totalChapterNum else it.toInt() + } + CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1) + } + } + cancelButton() + } + } + } + + fun showSimulatedReading() { + val book = ReadBook.book ?: return + val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val alertBinding = DialogSimulatedReadingBinding.inflate(layoutInflater).apply { + srEnabled.isChecked = book.getReadSimulating() + editStart.setText(book.getStartChapter().toString()) + editNum.setText(book.getDailyChapters().toString()) + startDate.setText(book.getStartDate()?.format(dateFormatter)) + startDate.isFocusable = false // 设置为false,不允许获得焦点 + startDate.isCursorVisible = false // 不显示光标 + startDate.setOnClickListener { + // 获取当前日期 + val localStartDate = LocalDate.parse(startDate.text) + // 创建 DatePickerDialog + val datePickerDialog = DatePickerDialog( + root.context, + { _, yy, mm, dayOfMonth -> + // 使用Java 8的日期和时间API来格式化日期 + val date = LocalDate.of(yy, mm + 1, dayOfMonth) // Java 8的LocalDate,月份从1开始 + val formattedDate = date.format(dateFormatter) + startDate.setText(formattedDate) + }, localStartDate.year, + localStartDate.monthValue - 1, + localStartDate.dayOfMonth + ) + datePickerDialog.show() + } + } + alert(titleResource = R.string.simulated_reading) { + customView { alertBinding.root } + okButton { + alertBinding.run { + val start = editStart.text!!.toString().let { + if (it.isEmpty()) 0 else it.toInt() + } + val num = editNum.text!!.toString().let { + if (it.isEmpty()) book.totalChapterNum else it.toInt() + } + val enabled = srEnabled.isChecked + val date = startDate.text!!.toString().let { + if (it.isEmpty()) LocalDate.now() + else LocalDate.parse(it, dateFormatter) + } + book.setStartDate(date) + book.setDailyChapters(num) + book.setStartChapter(start) + book.setReadSimulating(enabled) + book.save() + ReadBook.clearTextChapter() + viewModel.initData(intent) + } + } + cancelButton() + } + } + + fun showCharsetConfig() { + alert(R.string.set_charset) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "charset" + editView.setFilterValues(charsets) + editView.setText(ReadBook.book?.charset) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + ReadBook.setCharset(it) + } + } + cancelButton() + } + } + + fun showPageAnimConfig(success: () -> Unit) { + val items = arrayListOf() + items.add(getString(R.string.btn_default_s)) + items.add(getString(R.string.page_anim_cover)) + items.add(getString(R.string.page_anim_slide)) + items.add(getString(R.string.page_anim_simulation)) + items.add(getString(R.string.page_anim_scroll)) + items.add(getString(R.string.page_anim_none)) + selector(R.string.page_anim, items) { _, i -> + ReadBook.book?.setPageAnim(i - 1) + success() + } + } + + fun isPrevKey(keyCode: Int): Boolean { + if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + return false + } + val prevKeysStr = getPrefString(PreferKey.prevKeys) + return prevKeysStr?.split(",")?.contains(keyCode.toString()) ?: false + } + + fun isNextKey(keyCode: Int): Boolean { + if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + return false + } + val nextKeysStr = getPrefString(PreferKey.nextKeys) + return nextKeysStr?.split(",")?.contains(keyCode.toString()) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt new file mode 100755 index 000000000..84fe07340 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/ContentEditDialog.kt @@ -0,0 +1,170 @@ +package io.legado.app.ui.book.read + +import android.app.Application +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.DialogContentEditBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isLocal +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.ReadBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.applyTint +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 内容编辑 + */ +class ContentEditDialog : BaseDialogFragment(R.layout.dialog_content_edit) { + + val binding by viewBinding(DialogContentEditBinding::bind) + val viewModel by viewModels() + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = ReadBook.curTextChapter?.title + initMenu() + binding.toolBar.setOnClickListener { + lifecycleScope.launch { + val book = ReadBook.book ?: return@launch + val chapter = withContext(IO) { + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + } ?: return@launch + editTitle(chapter) + } + } + viewModel.loadStateLiveData.observe(viewLifecycleOwner) { + if (it) { + binding.rlLoading.visible() + } else { + binding.rlLoading.gone() + } + } + viewModel.initContent { + binding.contentView.setText(it) + binding.contentView.post { + binding.contentView.apply { + val lineIndex = layout.getLineForOffset(ReadBook.durChapterPos) + val lineHeight = layout.getLineTop(lineIndex) + scrollTo(0, lineHeight) + } + } + } + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.content_edit) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_save -> { + save() + dismiss() + } + R.id.menu_reset -> viewModel.initContent(true) { content -> + binding.contentView.setText(content) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + R.id.menu_copy_all -> requireContext() + .sendToClip("${binding.toolBar.title}\n${binding.contentView.text}") + } + return@setOnMenuItemClickListener true + } + } + + private fun editTitle(chapter: BookChapter) { + alert { + setTitle(R.string.edit) + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + alertBinding.editView.setText(chapter.title) + setCustomView(alertBinding.root) + okButton { + chapter.title = alertBinding.editView.text.toString() + lifecycleScope.launch { + withContext(IO) { + appDb.bookChapterDao.update(chapter) + } + binding.toolBar.title = chapter.getDisplayTitle() + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + } + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + save() + } + + private fun save() { + val content = binding.contentView.text?.toString() ?: return + Coroutine.async { + val book = ReadBook.book ?: return@async + val chapter = appDb.bookChapterDao + .getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: return@async + BookHelp.saveText(book, chapter, content) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + + class ContentEditViewModel(application: Application) : BaseViewModel(application) { + val loadStateLiveData = MutableLiveData() + var content: String? = null + + fun initContent(reset: Boolean = false, success: (String) -> Unit) { + execute { + val book = ReadBook.book ?: return@execute null + val chapter = appDb.bookChapterDao + .getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: return@execute null + if (reset) { + content = null + BookHelp.delContent(book, chapter) + if (!book.isLocal) ReadBook.bookSource?.let { bookSource -> + WebBook.getContentAwait(bookSource, book, chapter) + } + } + return@execute content ?: let { + val contentProcessor = ContentProcessor.get(book.name, book.origin) + val content = BookHelp.getContent(book, chapter) ?: return@let null + contentProcessor.getContent(book, chapter, content, includeTitle = false) + .toString() + } + }.onStart { + loadStateLiveData.postValue(true) + }.onSuccess { + content = it + success.invoke(it ?: "") + }.onFinally { + loadStateLiveData.postValue(false) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt new file mode 100755 index 000000000..e034009c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt @@ -0,0 +1,114 @@ +package io.legado.app.ui.book.read + +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.Item1lineTextBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.replace.edit.ReplaceEditActivity +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 起效的替换规则 + */ +class EffectiveReplacesDialog : BaseDialogFragment(R.layout.dialog_recycler_view) { + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by activityViewModels() + private val adapter by lazy { ReplaceAdapter(requireContext()) } + private val chineseConvert by lazy { ReplaceRule(0, "繁简转换") } + + private var isEdit = false + + private val editActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == AppCompatActivity.RESULT_OK) { + isEdit = true + } + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.setTitle(R.string.effective_replaces) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter + } + val effectiveReplaceRules = ReadBook.curTextChapter?.effectiveReplaceRules ?: emptyList() + if (AppConfig.chineseConverterType > 0) { + adapter.setItems(effectiveReplaceRules + chineseConvert) + } else { + adapter.setItems(effectiveReplaceRules) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + if (isEdit) { + viewModel.replaceRuleChanged() + } + } + + private fun showChineseConvertAlert() { + alert(titleResource = R.string.chinese_converter) { + items(resources.getStringArray(R.array.chinese_mode).toList()) { _, i -> + if (AppConfig.chineseConverterType != i) { + AppConfig.chineseConverterType = i + isEdit = true + } + } + } + } + + private inner class ReplaceAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding { + return Item1lineTextBinding.inflate(inflater, parent, false) + } + + override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) { + binding.root.setOnClickListener { + getItem(holder.layoutPosition)?.let { item -> + if (item == chineseConvert) { + showChineseConvertAlert() + return@let + } + editActivity.launch(ReplaceEditActivity.startIntent(requireContext(), item.id)) + } + } + } + + override fun convert( + holder: ItemViewHolder, + binding: Item1lineTextBinding, + item: ReplaceRule, + payloads: MutableList + ) { + binding.textView.text = item.name + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt new file mode 100755 index 000000000..6d05b665d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt @@ -0,0 +1,243 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.animation.Animation +import android.widget.FrameLayout +import android.widget.SeekBar +import androidx.core.view.isGone +import androidx.core.view.isVisible +import io.legado.app.R +import io.legado.app.databinding.ViewMangaMenuBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.source.getSourceType +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.model.ReadBook +import io.legado.app.model.ReadManga +import io.legado.app.ui.browser.WebViewActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.ConstraintModify +import io.legado.app.utils.activity +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.dpToPx +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.loadAnimation +import io.legado.app.utils.modifyBegin +import io.legado.app.utils.openUrl +import io.legado.app.utils.startActivity +import io.legado.app.utils.visible + +class MangaMenu @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : FrameLayout(context, attrs) { + private val binding = ViewMangaMenuBinding.inflate(LayoutInflater.from(context), this, true) + private val callBack: CallBack get() = activity as CallBack + var canShowMenu: Boolean = false + private val menuTopIn: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_top_in) + } + private val menuTopOut: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_top_out) + } + private val menuBottomIn: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_bottom_in) + } + private val menuBottomOut: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_bottom_out) + } + private var isMenuOutAnimating = false + private var bgColor = context.bottomBackground + + private val menuOutListener = object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + isMenuOutAnimating = true + binding.vwMenuBg.setOnClickListener(null) + } + + override fun onAnimationEnd(animation: Animation) { + this@MangaMenu.invisible() + binding.titleBar.invisible() + binding.bottomMenu.invisible() + isMenuOutAnimating = false + canShowMenu = false + callBack.upSystemUiVisibility(false) + } + + override fun onAnimationRepeat(animation: Animation) = Unit + } + private val menuInListener = object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + binding.tvSourceAction.text = + ReadManga.bookSource?.bookSourceName ?: context.getString(R.string.book_source) + callBack.upSystemUiVisibility(true) + binding.tvSourceAction.isGone = false + } + + @SuppressLint("RtlHardcoded") + override fun onAnimationEnd(animation: Animation) { + binding.run { + vwMenuBg.setOnClickListener { runMenuOut() } + } + } + + override fun onAnimationRepeat(animation: Animation) = Unit + } + + init { + initView() + bindEvent() + } + + private fun initView() = binding.run { + initAnimation() + val brightnessBackground = GradientDrawable() + brightnessBackground.cornerRadius = 5F.dpToPx() + brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f)) + if (AppConfig.isEInkMode) { + titleBar.setBackgroundResource(R.drawable.bg_eink_border_bottom) + bottomMenu.setBackgroundResource(R.drawable.bg_eink_border_top) + } else { + bottomMenu.setBackgroundColor(bgColor) + } + if (AppConfig.showReadTitleBarAddition) { + titleBarAddition.visible() + } else { + titleBarAddition.gone() + } + upBrightnessVwPos() + /** + * 确保视图不被导航栏遮挡 + */ + bottomMenu.applyNavigationBarPadding() + } + + private fun upBrightnessVwPos() { + if (AppConfig.brightnessVwPos) { + binding.root.modifyBegin() + .clear(R.id.ll_brightness, ConstraintModify.Anchor.LEFT) + .rightToRightOf(R.id.ll_brightness, R.id.vw_menu_root) + .commit() + } else { + binding.root.modifyBegin() + .clear(R.id.ll_brightness, ConstraintModify.Anchor.RIGHT) + .leftToLeftOf(R.id.ll_brightness, R.id.vw_menu_root) + .commit() + } + } + + private fun initAnimation() { + menuTopIn.setAnimationListener(menuInListener) + menuTopOut.setAnimationListener(menuOutListener) + } + + fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode) { + if (isMenuOutAnimating) { + return + } + if (this.isVisible) { + if (anim) { + binding.titleBar.startAnimation(menuTopOut) + binding.bottomMenu.startAnimation(menuBottomOut) + } else { + menuOutListener.onAnimationStart(menuBottomOut) + menuOutListener.onAnimationEnd(menuBottomOut) + } + } + } + + fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) { + this.visible() + binding.titleBar.visible() + binding.bottomMenu.visible() + if (anim) { + binding.titleBar.startAnimation(menuTopIn) + binding.bottomMenu.startAnimation(menuBottomIn) + } else { + menuInListener.onAnimationStart(menuBottomIn) + menuInListener.onAnimationEnd(menuBottomIn) + } + } + + + private fun bindEvent() = binding.run { + vwMenuBg.setOnClickListener { runMenuOut() } + titleBar.toolbar.setOnClickListener { + callBack.openBookInfoActivity() + } + val chapterViewClickListener = OnClickListener { + if (AppConfig.readUrlInBrowser) { + context.openUrl(tvChapterUrl.text.toString().substringBefore(",{")) + } else { + context.startActivity { + val url = tvChapterUrl.text.toString() + val bookSource = ReadBook.bookSource + putExtra("title", tvChapterName.text) + putExtra("url", url) + putExtra("sourceOrigin", bookSource?.bookSourceUrl) + putExtra("sourceName", bookSource?.bookSourceName) + putExtra("sourceType", bookSource?.getSourceType()) + } + } + } + val chapterViewLongClickListener = OnLongClickListener { + context.alert(R.string.open_fun) { + setMessage(R.string.use_browser_open) + okButton { + AppConfig.readUrlInBrowser = true + } + noButton { + AppConfig.readUrlInBrowser = false + } + } + true + } + tvChapterName.setOnClickListener(chapterViewClickListener) + tvChapterName.setOnLongClickListener(chapterViewLongClickListener) + tvChapterUrl.setOnClickListener(chapterViewClickListener) + tvChapterUrl.setOnLongClickListener(chapterViewLongClickListener) + + tvNext.setOnClickListener { + ReadManga.moveToNextChapter(true) + } + tvPre.setOnClickListener { + ReadManga.moveToPrevChapter(true) + } + + seekReadPage.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (fromUser) { + callBack.skipToPage(seekBar.progress) + } + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + binding.vwMenuBg.setOnClickListener(null) + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + binding.vwMenuBg.setOnClickListener { runMenuOut() } + } + }) + } + + fun upSeekBar(value: Int, count: Int) { + binding.seekReadPage.apply { + max = count.minus(1) + progress = value + } + } + + interface CallBack { + fun openBookInfoActivity() + fun upSystemUiVisibility(menuIsVisible: Boolean) + fun skipToPage(index: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt new file mode 100755 index 000000000..b1e55e234 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -0,0 +1,1732 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.os.Looper +import android.view.Gravity +import android.view.InputDevice +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import androidx.activity.addCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.get +import androidx.core.view.size +import androidx.lifecycle.lifecycleScope +import com.jaredrummler.android.colorpicker.ColorPickerDialogListener +import io.legado.app.BuildConfig +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.constant.Status +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.IntentData +import io.legado.app.help.TTS +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isEpub +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isLocalTxt +import io.legado.app.help.book.isMobi +import io.legado.app.help.book.removeType +import io.legado.app.help.book.update +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ReadTipConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.source.getSourceType +import io.legado.app.help.storage.Backup +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.localBook.EpubFile +import io.legado.app.model.localBook.MobiFile +import io.legado.app.receiver.NetworkChangedListener +import io.legado.app.receiver.TimeBatteryReceiver +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.bookmark.BookmarkDialog +import io.legado.app.ui.book.changesource.ChangeBookSourceDialog +import io.legado.app.ui.book.changesource.ChangeChapterSourceDialog +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.read.config.AutoReadDialog +import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR +import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR +import io.legado.app.ui.book.read.config.MoreConfigDialog +import io.legado.app.ui.book.read.config.ReadAloudDialog +import io.legado.app.ui.book.read.config.ReadStyleDialog +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR +import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.LayoutProgressListener +import io.legado.app.ui.book.searchContent.SearchContentActivity +import io.legado.app.ui.book.searchContent.SearchResult +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.book.toc.TocActivityResult +import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog +import io.legado.app.ui.browser.WebViewActivity +import io.legado.app.ui.dict.DictDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.replace.ReplaceRuleActivity +import io.legado.app.ui.replace.edit.ReplaceEditActivity +import io.legado.app.ui.widget.PopupAction +import io.legado.app.ui.widget.dialog.PhotoDialog +import io.legado.app.utils.ACache +import io.legado.app.utils.Debounce +import io.legado.app.utils.LogUtils +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.StartActivityContract +import io.legado.app.utils.applyOpenTint +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.dismissDialogFragment +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefString +import io.legado.app.utils.hexString +import io.legado.app.utils.iconItemOnLongClick +import io.legado.app.utils.invisible +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isTrue +import io.legado.app.utils.launch +import io.legado.app.utils.navigationBarGravity +import io.legado.app.utils.observeEvent +import io.legado.app.utils.observeEventSticky +import io.legado.app.utils.postEvent +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.startActivity +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.sysScreenOffTime +import io.legado.app.utils.throttle +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 阅读界面 + */ +class ReadBookActivity : BaseReadBookActivity(), + View.OnTouchListener, + ReadView.CallBack, + TextActionMenu.CallBack, + ContentTextView.CallBack, + PopupMenu.OnMenuItemClickListener, + ReadMenu.CallBack, + SearchMenu.CallBack, + ReadAloudDialog.CallBack, + ChangeBookSourceDialog.CallBack, + ChangeChapterSourceDialog.CallBack, + ReadBook.CallBack, + AutoReadDialog.CallBack, + TxtTocRuleDialog.CallBack, + ColorPickerDialogListener, + LayoutProgressListener { + + private val tocActivity = + registerForActivityResult(TocActivityResult()) { + it?.let { + viewModel.openChapter(it.first, it.second) + } + } + private val sourceEditActivity = + registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) { + if (it.resultCode == RESULT_OK) { + viewModel.upBookSource { + upMenuView() + } + } + } + private val replaceActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + viewModel.replaceRuleChanged() + } + } + private val searchContentActivity = + registerForActivityResult(StartActivityContract(SearchContentActivity::class.java)) { + val data = it.data ?: return@registerForActivityResult + val key = data.getLongExtra("key", System.currentTimeMillis()) + val index = data.getIntExtra("index", 0) + val searchResult = IntentData.get("searchResult$key") + val searchResultList = IntentData.get>("searchResultList$key") + if (searchResult != null && searchResultList != null) { + viewModel.searchContentQuery = searchResult.query + binding.searchMenu.upSearchResultList(searchResultList) + isShowingSearchResult = true + viewModel.searchResultIndex = index + binding.searchMenu.updateSearchResultIndex(index) + binding.searchMenu.selectedSearchResult?.let { currentResult -> + ReadBook.saveCurrentBookProgress() //退出全文搜索恢复此时进度 + skipToSearch(currentResult) + showActionMenu() + } + } + } + private val bookInfoActivity = + registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) { + if (it.resultCode == RESULT_OK) { + setResult(RESULT_DELETED) + super.finish() + } else { + ReadBook.loadOrUpContent() + } + } + private val selectImageDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + ACache.get().put(AppConst.imagePathKey, uri.toString()) + viewModel.saveImage(it.value, uri) + } + } + private var menu: Menu? = null + private var backupJob: Job? = null + private var tts: TTS? = null + val textActionMenu: TextActionMenu by lazy { + TextActionMenu(this, this) + } + private val popupAction: PopupAction by lazy { + PopupAction(this) + } + override val isInitFinish: Boolean get() = viewModel.isInitFinish + override val isScroll: Boolean get() = binding.readView.isScroll + private val isAutoPage get() = binding.readView.isAutoPage + override var isShowingSearchResult = false + override var isSelectingSearchResult = false + set(value) { + field = value && isShowingSearchResult + } + private val timeBatteryReceiver = TimeBatteryReceiver() + private var screenTimeOut: Long = 0 + private var loadStates: Boolean = false + override val pageFactory get() = binding.readView.pageFactory + override val pageDelegate get() = binding.readView.pageDelegate + override val headerHeight: Int get() = binding.readView.curPage.headerHeight + private val nextPageDebounce by lazy { Debounce { keyPage(PageDirection.NEXT) } } + private val prevPageDebounce by lazy { Debounce { keyPage(PageDirection.PREV) } } + private var bookChanged = false + private var pageChanged = false + private val handler by lazy { buildMainHandler() } + private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } } + private val executor = ReadBook.executor + private val upSeekBarThrottle = throttle(200) { + runOnUiThread { + upSeekBarProgress() + binding.readMenu.upSeekBar() + } + } + + //恢复跳转前进度对话框的交互结果 + private var confirmRestoreProcess: Boolean? = null + private val networkChangedListener by lazy { + NetworkChangedListener(this) + } + private var justInitData: Boolean = false + private var syncDialog: AlertDialog? = null + + @SuppressLint("ClickableViewAccessibility") + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + binding.cursorLeft.setColorFilter(accentColor) + binding.cursorRight.setColorFilter(accentColor) + binding.cursorLeft.setOnTouchListener(this) + binding.cursorRight.setOnTouchListener(this) + window.setBackgroundDrawable(null) + upScreenTimeOut() + ReadBook.register(this) + onBackPressedDispatcher.addCallback(this) { + if (isShowingSearchResult) { + exitSearchMenu() + restoreLastBookProcess() + return@addCallback + } + //拦截返回供恢复阅读进度 + if (ReadBook.lastBookProgress != null && confirmRestoreProcess != false) { + restoreLastBookProcess() + return@addCallback + } + if (BaseReadAloudService.isPlay()) { + ReadAloud.pause(this@ReadBookActivity) + toastOnUi(R.string.read_aloud_pause) + return@addCallback + } + if (isAutoPage) { + autoPageStop() + return@addCallback + } + if (getPrefBoolean("disableReturnKey") && !menuLayoutIsVisible) { + return@addCallback + } + finish() + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + viewModel.initReadBookConfig(intent) + Looper.myQueue().addIdleHandler { + viewModel.initData(intent) + false + } + justInitData = true + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + viewModel.initData(intent) + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + upSystemUiVisibility() + if (hasFocus) { + binding.readMenu.upBrightnessState() + } else if (!menuLayoutIsVisible) { + ReadBook.cancelPreDownloadTask() + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + upSystemUiVisibility() + binding.readView.upStatusBar() + } + + override fun onTopResumedActivityChanged(isTopResumedActivity: Boolean) { + if (!isTopResumedActivity) { + ReadBook.cancelPreDownloadTask() + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + override fun onResume() { + super.onResume() + ReadBook.readStartTime = System.currentTimeMillis() + if (bookChanged) { + bookChanged = false + ReadBook.callBack = this + viewModel.initData(intent) + justInitData = true + } else { + //web端阅读时,app处于阅读界面,本地记录会覆盖web保存的进度,在此处恢复 + ReadBook.webBookProgress?.let { + ReadBook.setProgress(it) + ReadBook.webBookProgress = null + } + } + upSystemUiVisibility() + registerReceiver(timeBatteryReceiver, timeBatteryReceiver.filter) + binding.readView.upTime() + screenOffTimerStart() + // 网络监听,当从无网切换到网络环境时同步进度(注意注册的同时就会收到监听,因此界面激活时无需重复执行同步操作) + networkChangedListener.register() + networkChangedListener.onNetworkChanged = { + // 当网络是可用状态且无需初始化时同步进度(初始化中已有同步进度逻辑) + if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData && ReadBook.inBookshelf) { + ReadBook.syncProgress({ progress -> sureNewProgress(progress) }) + } + } + } + + override fun onPause() { + super.onPause() + autoPageStop() + backupJob?.cancel() + ReadBook.saveRead() + ReadBook.cancelPreDownloadTask() + unregisterReceiver(timeBatteryReceiver) + upSystemUiVisibility() + if (!BuildConfig.DEBUG && ReadBook.inBookshelf) { + if (AppConfig.syncBookProgressPlus) { + ReadBook.syncProgress() + } else { + ReadBook.uploadProgress() + } + } + if (!BuildConfig.DEBUG) { + Backup.autoBack(this) + } + justInitData = false + networkChangedListener.unRegister() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_read, menu) + menu.iconItemOnLongClick(R.id.menu_change_source) { + PopupMenu(this, it).apply { + inflate(R.menu.book_read_change_source) + this.menu.applyOpenTint(this@ReadBookActivity) + setOnMenuItemClickListener(this@ReadBookActivity) + }.show() + } + menu.iconItemOnLongClick(R.id.menu_refresh) { + PopupMenu(this, it).apply { + inflate(R.menu.book_read_refresh) + this.menu.applyOpenTint(this@ReadBookActivity) + setOnMenuItemClickListener(this@ReadBookActivity) + }.show() + } + binding.readMenu.refreshMenuColorFilter() + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_same_title_removed)?.isChecked = + ReadBook.curTextChapter?.sameTitleRemoved == true + return super.onMenuOpened(featureId, menu) + } + + /** + * 更新菜单 + */ + private fun upMenu() { + val menu = menu ?: return + val book = ReadBook.book ?: return + val onLine = !book.isLocal + for (i in 0 until menu.size) { + val item = menu[i] + when (item.groupId) { + R.id.menu_group_on_line -> item.isVisible = onLine + R.id.menu_group_local -> item.isVisible = !onLine + R.id.menu_group_text -> item.isVisible = book.isLocalTxt + R.id.menu_group_epub -> item.isVisible = book.isEpub + else -> when (item.itemId) { + R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() + R.id.menu_re_segment -> item.isChecked = book.getReSegment() +// R.id.menu_enable_review -> { +// item.isVisible = BuildConfig.DEBUG +// item.isChecked = AppConfig.enableReview +// } + + R.id.menu_reverse_content -> item.isVisible = onLine + R.id.menu_del_ruby_tag -> item.isChecked = book.getDelTag(Book.rubyTag) + R.id.menu_del_h_tag -> item.isChecked = book.getDelTag(Book.hTag) + } + } + } + lifecycleScope.launch { + val show = ReadBook.inBookshelf && withContext(IO) { + AppWebDav.isOk + } + menu.findItem(R.id.menu_get_progress)?.isVisible = show + menu.findItem(R.id.menu_cover_progress)?.isVisible = show + } + } + + /** + * 菜单 + */ + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_change_source, + R.id.menu_book_change_source -> { + binding.readMenu.runMenuOut() + ReadBook.book?.let { + showDialogFragment(ChangeBookSourceDialog(it.name, it.author)) + } + } + + R.id.menu_chapter_change_source -> lifecycleScope.launch { + val book = ReadBook.book ?: return@launch + val chapter = + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: return@launch + binding.readMenu.runMenuOut() + showDialogFragment( + ChangeChapterSourceDialog(book.name, book.author, chapter.index, chapter.title) + ) + } + + R.id.menu_refresh, + R.id.menu_refresh_dur -> { + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + ReadBook.curTextChapter = null + binding.readView.upContent() + viewModel.refreshContentDur(it) + } + } + } + + R.id.menu_refresh_after -> { + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + ReadBook.clearTextChapter() + binding.readView.upContent() + viewModel.refreshContentAfter(it) + } + } + } + + R.id.menu_refresh_all -> { + if (ReadBook.bookSource == null) { + upContent() + } else { + ReadBook.book?.let { + refreshContentAll(it) + } + } + } + + R.id.menu_download -> showDownloadDialog() + R.id.menu_add_bookmark -> addBookmark() + R.id.menu_simulated_reading -> showSimulatedReading() + R.id.menu_edit_content -> showDialogFragment(ContentEditDialog()) + R.id.menu_update_toc -> ReadBook.book?.let { + if (it.isEpub) { + BookHelp.clearCache(it) + EpubFile.clear() + } + if (it.isMobi) { + MobiFile.clear() + } + loadChapterList(it) + } + + R.id.menu_enable_replace -> changeReplaceRuleState() + R.id.menu_re_segment -> ReadBook.book?.let { + it.setReSegment(!it.getReSegment()) + item.isChecked = it.getReSegment() + ReadBook.loadContent(false) + } + +// R.id.menu_enable_review -> { +// AppConfig.enableReview = !AppConfig.enableReview +// item.isChecked = AppConfig.enableReview +// ReadBook.loadContent(false) +// } + + R.id.menu_del_ruby_tag -> ReadBook.book?.let { + item.isChecked = !item.isChecked + if (item.isChecked) { + it.addDelTag(Book.rubyTag) + } else { + it.removeDelTag(Book.rubyTag) + } + refreshContentAll(it) + } + + R.id.menu_del_h_tag -> ReadBook.book?.let { + item.isChecked = !item.isChecked + if (item.isChecked) { + it.addDelTag(Book.hTag) + } else { + it.removeDelTag(Book.hTag) + } + refreshContentAll(it) + } + + R.id.menu_page_anim -> showPageAnimConfig { + binding.readView.upPageAnim() + ReadBook.loadContent(false) + } + + R.id.menu_log -> showDialogFragment() + R.id.menu_toc_regex -> showDialogFragment( + TxtTocRuleDialog(ReadBook.book?.tocUrl) + ) + + R.id.menu_reverse_content -> ReadBook.book?.let { + viewModel.reverseContent(it) + } + + R.id.menu_set_charset -> showCharsetConfig() + R.id.menu_image_style -> { + val imgStyles = + arrayListOf( + Book.imgStyleDefault, Book.imgStyleFull, Book.imgStyleText, + Book.imgStyleSingle + ) + selector( + R.string.image_style, + imgStyles + ) { _, index -> + val imageStyle = imgStyles[index] + ReadBook.book?.setImageStyle(imageStyle) + if (imageStyle == Book.imgStyleSingle) { + ReadBook.book?.setPageAnim(0) // 切换图片样式single后,自动切换为覆盖 + binding.readView.upPageAnim() + } + ReadBook.loadContent(false) + } + } + + R.id.menu_get_progress -> ReadBook.book?.let { + viewModel.syncBookProgress(it) { progress -> + sureSyncProgress(progress) + } + } + + R.id.menu_cover_progress -> ReadBook.book?.let { + ReadBook.uploadProgress(true) { toastOnUi(R.string.upload_book_success) } + } + + R.id.menu_same_title_removed -> { + ReadBook.book?.let { + val contentProcessor = ContentProcessor.get(it) + val textChapter = ReadBook.curTextChapter + if (textChapter != null + && !textChapter.sameTitleRemoved + && !contentProcessor.removeSameTitleCache.contains( + textChapter.chapter.getFileName("nr") + ) + ) { + toastOnUi("未找到可移除的重复标题") + } + } + viewModel.reverseRemoveSameTitle() + } + + R.id.menu_effective_replaces -> showDialogFragment() + + R.id.menu_help -> showHelp() + } + return super.onCompatOptionsItemSelected(item) + } + + private fun refreshContentAll(book: Book) { + ReadBook.clearTextChapter() + binding.readView.upContent() + viewModel.refreshContentAll(book) + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + return onCompatOptionsItemSelected(item) + } + + /** + * 按键拦截,显示菜单 + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val keyCode = event.keyCode + val action = event.action + val isDown = action == 0 + + if (keyCode == KeyEvent.KEYCODE_MENU) { + if (isDown && !binding.readMenu.canShowMenu) { + binding.readMenu.runMenuIn() + return true + } + if (!isDown && !binding.readMenu.canShowMenu) { + binding.readMenu.canShowMenu = true + return true + } + } + return super.dispatchKeyEvent(event) + } + + /** + * 鼠标滚轮事件 + */ + override fun onGenericMotionEvent(event: MotionEvent): Boolean { + if (0 != (event.source and InputDevice.SOURCE_CLASS_POINTER)) { + if (event.action == MotionEvent.ACTION_SCROLL) { + val axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL) + LogUtils.d("onGenericMotionEvent", "axisValue = $axisValue") + // 获得垂直坐标上的滚动方向 + if (axisValue < 0.0f) { // 滚轮向下滚 + mouseWheelPage(PageDirection.NEXT) + } else { // 滚轮向上滚 + mouseWheelPage(PageDirection.PREV) + } + return true + } + } + return super.onGenericMotionEvent(event) + } + + /** + * 按键事件 + */ + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (menuLayoutIsVisible) { + return super.onKeyDown(keyCode, event) + } + val longPress = event.repeatCount > 0 + when { + isPrevKey(keyCode) -> { + handleKeyPage(PageDirection.PREV, longPress) + return true + } + + isNextKey(keyCode) -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + } + when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP -> if (volumeKeyPage(PageDirection.PREV, longPress)) { + return true + } + + KeyEvent.KEYCODE_VOLUME_DOWN -> if (volumeKeyPage(PageDirection.NEXT, longPress)) { + return true + } + + KeyEvent.KEYCODE_PAGE_UP -> { + handleKeyPage(PageDirection.PREV, longPress) + return true + } + + KeyEvent.KEYCODE_PAGE_DOWN -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + + KeyEvent.KEYCODE_SPACE -> { + handleKeyPage(PageDirection.NEXT, longPress) + return true + } + } + + return super.onKeyDown(keyCode, event) + } + + /** + * 松开按键事件 + */ + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> { + if (volumeKeyPage(PageDirection.NONE, false)) { + return true + } + } + + } + return super.onKeyUp(keyCode, event) + } + + /** + * view触摸,文字选择 + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean = binding.run { + if (!binding.readView.isTextSelected) { + return false + } + when (event.action) { + MotionEvent.ACTION_DOWN -> textActionMenu.dismiss() + MotionEvent.ACTION_MOVE -> { + when (v.id) { + R.id.cursor_left -> if (!readView.curPage.getReverseStartCursor()) { + readView.curPage.selectStartMove( + event.rawX + cursorLeft.width, + event.rawY - cursorLeft.height + ) + } else { + readView.curPage.selectEndMove( + event.rawX - cursorRight.width, + event.rawY - cursorRight.height + ) + } + + R.id.cursor_right -> if (readView.curPage.getReverseEndCursor()) { + readView.curPage.selectStartMove( + event.rawX + cursorLeft.width, + event.rawY - cursorLeft.height + ) + } else { + readView.curPage.selectEndMove( + event.rawX - cursorRight.width, + event.rawY - cursorRight.height + ) + } + } + } + + MotionEvent.ACTION_UP -> { + readView.curPage.resetReverseCursor() + showTextActionMenu() + } + } + return true + } + + /** + * 更新文字选择开始位置 + */ + override fun upSelectedStart(x: Float, y: Float, top: Float) = binding.run { + cursorLeft.x = x - cursorLeft.width + cursorLeft.y = y + cursorLeft.visible(true) + textMenuPosition.x = x + textMenuPosition.y = top + } + + /** + * 更新文字选择结束位置 + */ + override fun upSelectedEnd(x: Float, y: Float) = binding.run { + cursorRight.x = x + cursorRight.y = y + cursorRight.visible(true) + } + + /** + * 取消文字选择 + */ + override fun onCancelSelect() = binding.run { + cursorLeft.invisible() + cursorRight.invisible() + textActionMenu.dismiss() + } + + override fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean { + return binding.readView.onTouchEvent(event) + } + + /** + * 显示文本操作菜单 + */ + override fun showTextActionMenu() { + val navigationBarHeight = + if (!ReadBookConfig.hideNavigationBar && navigationBarGravity == Gravity.BOTTOM) + binding.navigationBar.height else 0 + textActionMenu.show( + binding.textMenuPosition, + binding.root.height + navigationBarHeight, + binding.textMenuPosition.x.toInt(), + binding.textMenuPosition.y.toInt(), + binding.cursorLeft.y.toInt() + binding.cursorLeft.height, + binding.cursorRight.x.toInt(), + binding.cursorRight.y.toInt() + binding.cursorRight.height + ) + } + + /** + * 当前选择的文本 + */ + override val selectedText: String get() = binding.readView.getSelectText() + + /** + * 文本选择菜单操作 + */ + override fun onMenuItemSelected(itemId: Int): Boolean { + when (itemId) { + R.id.menu_aloud -> when (AppConfig.contentSelectSpeakMod) { + 1 -> lifecycleScope.launch { + binding.readView.aloudStartSelect() + } + + else -> speak(binding.readView.getSelectText()) + } + + R.id.menu_bookmark -> binding.readView.curPage.let { + val bookmark = it.createBookmark() + if (bookmark == null) { + toastOnUi(R.string.create_bookmark_error) + } else { + showDialogFragment(BookmarkDialog(bookmark)) + } + return true + } + + R.id.menu_replace -> { + val scopes = arrayListOf() + ReadBook.book?.name?.let { + scopes.add(it) + } + ReadBook.bookSource?.bookSourceUrl?.let { + scopes.add(it) + } + val text = selectedText.lineSequence().map { it.trim() }.joinToString("\n") + replaceActivity.launch( + ReplaceEditActivity.startIntent( + this, + pattern = text, + scope = scopes.joinToString(";") + ) + ) + return true + } + + R.id.menu_search_content -> { + viewModel.searchContentQuery = selectedText + openSearchActivity(selectedText) + return true + } + + R.id.menu_dict -> { + showDialogFragment(DictDialog(selectedText)) + return true + } + } + return false + } + + /** + * 文本选择菜单操作完成 + */ + override fun onMenuActionFinally() = binding.run { + textActionMenu.dismiss() + readView.cancelSelect() + } + + private fun speak(text: String) { + if (tts == null) { + tts = TTS() + } + tts?.speak(text) + } + + /** + * 鼠标滚轮翻页 + */ + private fun mouseWheelPage(direction: PageDirection) { + if (menuLayoutIsVisible || !AppConfig.mouseWheelPage) { + return + } + keyPageDebounce(direction, mouseWheel = true, longPress = false) + } + + /** + * 音量键翻页 + */ + private fun volumeKeyPage(direction: PageDirection, longPress: Boolean): Boolean { + if (!AppConfig.volumeKeyPage) { + return false + } + if (!AppConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) { + return false + } + handleKeyPage(direction, longPress) + return true + } + + private fun handleKeyPage(direction: PageDirection, longPress: Boolean) { + if (AppConfig.keyPageOnLongPress || direction == PageDirection.NONE) { + keyPage(direction) + } else { + keyPageDebounce(direction, longPress = longPress) + } + } + + private fun keyPageDebounce( + direction: PageDirection, + mouseWheel: Boolean = false, + longPress: Boolean + ) { + if (longPress) { + return + } + nextPageDebounce.apply { + wait = if (mouseWheel) 200L else 600L + leading = !mouseWheel + trailing = mouseWheel + } + prevPageDebounce.apply { + wait = if (mouseWheel) 200L else 600L + leading = !mouseWheel + trailing = mouseWheel + } + when (direction) { + PageDirection.NEXT -> nextPageDebounce.invoke() + PageDirection.PREV -> prevPageDebounce.invoke() + else -> {} + } + } + + private fun keyPage(direction: PageDirection) { + binding.readView.cancelSelect() + binding.readView.pageDelegate?.isCancel = false + binding.readView.pageDelegate?.keyTurnPage(direction) + } + + override fun upMenuView() { + handler.post { + upMenu() + binding.readMenu.upBookView() + } + } + + override fun loadChapterList(book: Book) { + ReadBook.upMsg(getString(R.string.toc_updateing)) + viewModel.loadChapterList(book) + } + + /** + * 内容加载完成 + */ + override fun contentLoadFinish() { + if (intent.getBooleanExtra("readAloud", false)) { + intent.removeExtra("readAloud") + ReadBook.readAloud() + } + loadStates = true + } + + /** + * 更新内容 + */ + override fun upContent( + relativePosition: Int, + resetPageOffset: Boolean, + success: (() -> Unit)? + ) { + lifecycleScope.launch { + binding.readView.upContent(relativePosition, resetPageOffset) + if (relativePosition == 0) { + upSeekBarProgress() + } + loadStates = false + success?.invoke() + } + } + + override suspend fun upContentAwait( + relativePosition: Int, + resetPageOffset: Boolean, + success: (() -> Unit)? + ) = withContext(Main.immediate) { + binding.readView.upContent(relativePosition, resetPageOffset) + if (relativePosition == 0) { + upSeekBarProgress() + } + loadStates = false + } + + override fun upPageAnim(upRecorder: Boolean) { + lifecycleScope.launch { + binding.readView.upPageAnim(upRecorder) + } + } + + override fun notifyBookChanged() { + bookChanged = true + if (!ReadBook.inBookshelf) { + viewModel.removeFromBookshelf { super.finish() } + } + } + + override fun cancelSelect() { + runOnUiThread { + binding.readView.cancelSelect() + } + } + + /** + * 页面改变 + */ + override fun pageChanged() { + pageChanged = true + binding.readView.onPageChange() + handler.post { + upSeekBarProgress() + } + executor.execute { + startBackupJob() + } + } + + /** + * 更新进度条位置 + */ + private fun upSeekBarProgress() { + val progress = when (AppConfig.progressBarBehavior) { + "page" -> ReadBook.durPageIndex + else /* chapter */ -> ReadBook.durChapterIndex + } + binding.readMenu.setSeekPage(progress) + } + + /** + * 显示菜单 + */ + override fun showMenuBar() { + binding.readMenu.runMenuIn() + } + + override val oldBook: Book? + get() = ReadBook.book + + override fun changeTo(source: BookSource, book: Book, toc: List) { + if (!book.isAudio) { + viewModel.changeTo(book, toc) + } else { + ReadAloud.stop(this) + lifecycleScope.launch { + withContext(IO) { + ReadBook.book?.migrateTo(book, toc) + book.removeType(BookType.updateError) + ReadBook.book?.delete() + appDb.bookDao.insert(book) + } + startActivityForBook(book) + finish() + } + } + } + + override fun replaceContent(content: String) { + ReadBook.book?.let { + viewModel.saveContent(it, content) + } + } + + override fun showActionMenu() { + when { + BaseReadAloudService.isRun -> showReadAloudDialog() + isAutoPage -> showDialogFragment() + isShowingSearchResult -> binding.searchMenu.runMenuIn() + else -> binding.readMenu.runMenuIn() + } + } + + /** + * 显示朗读菜单 + */ + override fun showReadAloudDialog() { + showDialogFragment() + } + + /** + * 自动翻页 + */ + override fun autoPage() { + ReadAloud.stop(this) + if (isAutoPage) { + autoPageStop() + } else { + binding.readView.autoPager.start() + binding.readMenu.setAutoPage(true) + screenTimeOut = -1L + screenOffTimerStart() + } + } + + override fun autoPageStop() { + if (isAutoPage) { + binding.readView.autoPager.stop() + binding.readMenu.setAutoPage(false) + dismissDialogFragment() + upScreenTimeOut() + } + } + + override fun openSourceEditActivity() { + ReadBook.bookSource?.let { + sourceEditActivity.launch { + putExtra("sourceUrl", it.bookSourceUrl) + } + } + } + + override fun openBookInfoActivity() { + ReadBook.book?.let { + bookInfoActivity.launch { + putExtra("name", it.name) + putExtra("author", it.author) + } + } + } + + /** + * 替换 + */ + override fun openReplaceRule() { + replaceActivity.launch(Intent(this, ReplaceRuleActivity::class.java)) + } + + /** + * 打开目录 + */ + override fun openChapterList() { + ReadBook.book?.let { + tocActivity.launch(it.bookUrl) + } + } + + /** + * 打开搜索界面 + */ + override fun openSearchActivity(searchWord: String?) { + val book = ReadBook.book ?: return + searchContentActivity.launch { + putExtra("bookUrl", book.bookUrl) + putExtra("searchWord", searchWord ?: viewModel.searchContentQuery) + putExtra("searchResultIndex", viewModel.searchResultIndex) + viewModel.searchResultList?.first()?.let { + if (it.query == viewModel.searchContentQuery) { + IntentData.put("searchResultList", viewModel.searchResultList) + } + } + } + } + + /** + * 禁用书源 + */ + override fun disableSource() { + viewModel.disableSource() + } + + /** + * 显示阅读样式配置 + */ + override fun showReadStyle() { + showDialogFragment() + } + + /** + * 显示更多设置 + */ + override fun showMoreSetting() { + showDialogFragment() + } + + override fun showSearchSetting() { + showDialogFragment() + } + + /** + * 更新状态栏,导航栏 + */ + override fun upSystemUiVisibility() { + upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible, bottomDialog > 0) + upNavigationBarColor() + } + + // 退出全文搜索 + override fun exitSearchMenu() { + if (isShowingSearchResult) { + isShowingSearchResult = false + binding.searchMenu.invalidate() + binding.searchMenu.invisible() + ReadBook.clearSearchResult() + binding.readView.cancelSelect(true) + } + } + + /* 恢复到 全文搜索/进度条跳转前的位置 */ + private fun restoreLastBookProcess() { + if (confirmRestoreProcess == true) { + ReadBook.restoreLastBookProgress() + } else if (confirmRestoreProcess == null) { + alert(R.string.draw) { + setMessage(R.string.restore_last_book_process) + yesButton { + confirmRestoreProcess = true + ReadBook.restoreLastBookProgress() //恢复启动全文搜索前的进度 + } + noButton { + ReadBook.lastBookProgress = null + confirmRestoreProcess = false + } + onCancelled { + ReadBook.lastBookProgress = null + confirmRestoreProcess = false + } + } + } + } + + override fun showLogin() { + ReadBook.bookSource?.let { + startActivity { + putExtra("type", "bookSource") + putExtra("key", it.bookSourceUrl) + } + } + } + + override fun payAction() { + val book = ReadBook.book ?: return + if (book.isLocal) return + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + if (chapter == null) { + toastOnUi("no chapter") + return + } + alert(R.string.chapter_pay) { + setMessage(chapter.title) + yesButton { + Coroutine.async(lifecycleScope) { + val source = + ReadBook.bookSource ?: throw NoStackTraceException("no book source") + val payAction = source.getContentRule().payAction + if (payAction.isNullOrBlank()) { + throw NoStackTraceException("no pay action") + } + val analyzeRule = AnalyzeRule(book, source) + analyzeRule.setCoroutineContext(coroutineContext) + analyzeRule.setBaseUrl(chapter.url) + analyzeRule.setChapter(chapter) + analyzeRule.evalJS(payAction).toString() + }.onSuccess(IO) { + if (it.isAbsUrl()) { + startActivity { + val bookSource = ReadBook.bookSource + putExtra("title", getString(R.string.chapter_pay)) + putExtra("url", it) + putExtra("sourceOrigin", bookSource?.bookSourceUrl) + putExtra("sourceName", bookSource?.bookSourceName) + putExtra("sourceType", bookSource?.getSourceType()) + } + } else if (it.isTrue()) { + //购买成功后刷新目录 + ReadBook.book?.let { + ReadBook.curTextChapter = null + BookHelp.delContent(book, chapter) + loadChapterList(book) + } + } + }.onError { + AppLog.put("执行购买操作出错\n${it.localizedMessage}", it, true) + } + } + noButton() + } + } + + /** + * 朗读按钮 + */ + override fun onClickReadAloud() { + autoPageStop() + when { + !BaseReadAloudService.isRun -> { + ReadAloud.upReadAloudClass() + val scrollPageAnim = ReadBook.pageAnim() == 3 + if (scrollPageAnim) { + val pos = binding.readView.getReadAloudPos() + if (pos != null) { + val (index, line) = pos + if (ReadBook.durChapterIndex != index) { + ReadBook.openChapter(index, line.chapterPosition, false) { + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.durChapterPos = line.chapterPosition + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.readAloud() + } + } else { + ReadBook.readAloud() + } + } + + BaseReadAloudService.pause -> { + val scrollPageAnim = ReadBook.pageAnim() == 3 + if (scrollPageAnim && pageChanged) { + pageChanged = false + val pos = binding.readView.getReadAloudPos() + if (pos != null) { + val (index, line) = pos + if (ReadBook.durChapterIndex != index) { + ReadBook.openChapter(index, line.chapterPosition, false) { + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.durChapterPos = line.chapterPosition + ReadBook.readAloud(startPos = line.pagePosition) + } + } else { + ReadBook.readAloud() + } + } else { + ReadAloud.resume(this) + } + } + + else -> ReadAloud.pause(this) + } + } + + override fun showHelp() { + showHelp("readMenuHelp") + } + + /** + * 长按图片 + */ + @SuppressLint("RtlHardcoded") + override fun onImageLongPress(x: Float, y: Float, src: String) { + popupAction.setItems( + listOf( + SelectItem(getString(R.string.show), "show"), + SelectItem(getString(R.string.refresh), "refresh"), + SelectItem(getString(R.string.action_save), "save"), + SelectItem(getString(R.string.menu), "menu"), + SelectItem(getString(R.string.select_folder), "selectFolder") + ) + ) + popupAction.onActionClick = { + when (it) { + "show" -> showDialogFragment(PhotoDialog(src)) + "refresh" -> viewModel.refreshImage(src) + "save" -> { + val path = ACache.get().getAsString(AppConst.imagePathKey) + if (path.isNullOrEmpty()) { + selectImageDir.launch { + value = src + } + } else { + viewModel.saveImage(src, Uri.parse(path)) + } + } + + "menu" -> showActionMenu() + "selectFolder" -> selectImageDir.launch() + } + popupAction.dismiss() + } + val navigationBarHeight = + if (!ReadBookConfig.hideNavigationBar && navigationBarGravity == Gravity.BOTTOM) + binding.navigationBar.height else 0 + popupAction.showAtLocation( + binding.readView, Gravity.BOTTOM or Gravity.LEFT, x.toInt(), + binding.root.height + navigationBarHeight - y.toInt() + ) + } + + /** + * colorSelectDialog + */ + override fun onColorSelected(dialogId: Int, color: Int) = ReadBookConfig.durConfig.run { + when (dialogId) { + TEXT_COLOR -> { + setCurTextColor(color) + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6, 9, 11)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + + BG_COLOR -> { + setCurBg(0, "#${color.hexString}") + postEvent(EventBus.UP_CONFIG, arrayListOf(1)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + + TIP_COLOR -> { + ReadTipConfig.tipColor = color + postEvent(EventBus.TIP_COLOR, "") + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + + TIP_DIVIDER_COLOR -> { + ReadTipConfig.tipDividerColor = color + postEvent(EventBus.TIP_COLOR, "") + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + } + } + + /** + * colorSelectDialog + */ + override fun onDialogDismissed(dialogId: Int) = Unit + + override fun onTocRegexDialogResult(tocRegex: String) { + ReadBook.book?.let { + it.tocUrl = tocRegex + loadChapterList(it) + } + } + + private fun sureSyncProgress(progress: BookProgress) { + alert(R.string.get_book_progress) { + setMessage(R.string.current_progress_exceeds_cloud) + okButton { + ReadBook.setProgress(progress) + } + noButton() + } + } + + /* 进度条跳转到指定章节 */ + override fun skipToChapter(index: Int) { + ReadBook.saveCurrentBookProgress() //退出章节跳转恢复此时进度 + viewModel.openChapter(index) + } + + /* 全文搜索跳转 */ + override fun navigateToSearch(searchResult: SearchResult, index: Int) { + viewModel.searchResultIndex = index + skipToSearch(searchResult) + } + + override fun onMenuShow() { + binding.readView.autoPager.pause() + } + + override fun onMenuHide() { + binding.readView.autoPager.resume() + } + + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + upSeekBarThrottle.invoke() + binding.readView.onLayoutPageCompleted(index, page) + } + + /* 全文搜索跳转 */ + private fun skipToSearch(searchResult: SearchResult) { + if (searchResult.chapterIndex != ReadBook.durChapterIndex) { + viewModel.openChapter(searchResult.chapterIndex) { + jumpToPosition(searchResult) + } + } else { + jumpToPosition(searchResult) + } + } + + private fun jumpToPosition(searchResult: SearchResult) { + val curTextChapter = ReadBook.curTextChapter ?: return + binding.searchMenu.updateSearchInfo() + val (pageIndex, lineIndex, charIndex, addLine, charIndex2) = + viewModel.searchResultPositions(curTextChapter, searchResult) + ReadBook.skipToPage(pageIndex) { + isSelectingSearchResult = true + binding.readView.curPage.selectStartMoveIndex(0, lineIndex, charIndex) + when (addLine) { + 0 -> binding.readView.curPage.selectEndMoveIndex( + 0, + lineIndex, + charIndex + viewModel.searchContentQuery.length - 1 + ) + + 1 -> binding.readView.curPage.selectEndMoveIndex( + 0, lineIndex + 1, charIndex2 + ) + //consider change page, jump to scroll position + -1 -> binding.readView.curPage.selectEndMoveIndex(1, 0, charIndex2) + } + binding.readView.isTextSelected = true + isSelectingSearchResult = false + } + } + + override fun addBookmark() { + val book = ReadBook.book + val page = ReadBook.curTextChapter?.getPage(ReadBook.durPageIndex) + if (book != null && page != null) { + val bookmark = book.createBookMark().apply { + chapterIndex = ReadBook.durChapterIndex + chapterPos = ReadBook.durChapterPos + chapterName = page.title + bookText = page.text.trim() + } + showDialogFragment(BookmarkDialog(bookmark)) + } + } + + override fun changeReplaceRuleState() { + ReadBook.book?.let { + it.setUseReplaceRule(!it.getUseReplaceRule()) + ReadBook.saveRead() + menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.getUseReplaceRule() + viewModel.replaceRuleChanged() + } + } + + private fun startBackupJob() { + backupJob?.cancel() + backupJob = lifecycleScope.launch(IO) { + delay(300000) + ReadBook.book?.let { + AppWebDav.uploadBookProgress(it) + ensureActive() + it.update() + Backup.autoBack(this@ReadBookActivity) + } + } + } + + override fun sureNewProgress(progress: BookProgress) { + syncDialog?.dismiss() + syncDialog = alert(R.string.get_book_progress) { + setMessage(R.string.cloud_progress_exceeds_current) + okButton { + ReadBook.setProgress(progress) + } + noButton() + } + } + + override fun finish() { + val book = ReadBook.book ?: return super.finish() + + if (ReadBook.inBookshelf) { + return super.finish() + } + + if (!AppConfig.showAddToShelfAlert) { + viewModel.removeFromBookshelf { super.finish() } + } else { + alert(title = getString(R.string.add_to_bookshelf)) { + setMessage(getString(R.string.check_add_bookshelf, book.name)) + okButton { + ReadBook.book?.removeType(BookType.notShelf) + ReadBook.book?.save() + ReadBook.inBookshelf = true + setResult(RESULT_OK) + } + noButton { viewModel.removeFromBookshelf { super.finish() } } + } + } + } + + override fun onDestroy() { + super.onDestroy() + tts?.clearTts() + textActionMenu.dismiss() + popupAction.dismiss() + binding.readView.onDestroy() + ReadBook.unregister(this) + if (!ReadBook.inBookshelf && !isChangingConfigurations) { + viewModel.removeFromBookshelf(null) + } + if (!BuildConfig.DEBUG) { + Backup.autoBack(this) + } + } + + override fun observeLiveBus() = binding.run { + observeEvent(EventBus.TIME_CHANGED) { readView.upTime() } + observeEvent(EventBus.BATTERY_CHANGED) { readView.upBattery(it) } + observeEvent(EventBus.MEDIA_BUTTON) { + if (it) { + onClickReadAloud() + } else { + ReadBook.readAloud(!BaseReadAloudService.pause) + } + } + observeEvent>(EventBus.UP_CONFIG) { + it.forEach { value -> + when (value) { + 0 -> upSystemUiVisibility() + 1 -> readView.upBg() + 2 -> readView.upStyle() + 3 -> readView.upBgAlpha() + 4 -> readView.upPageSlopSquare() + 5 -> if (isInitFinish) ReadBook.loadContent(resetPageOffset = false) + 6 -> readView.upContent(resetPageOffset = false) + 8 -> ChapterProvider.upStyle() + 9 -> readView.invalidateTextPage() + 10 -> ChapterProvider.upLayout() + 11 -> readView.submitRenderTask() + } + } + } + observeEvent(EventBus.ALOUD_STATE) { + if (it == Status.STOP || it == Status.PAUSE) { + ReadBook.curTextChapter?.let { textChapter -> + val page = textChapter.getPageByReadPos(ReadBook.durChapterPos) + if (page != null) { + page.removePageAloudSpan() + readView.upContent(resetPageOffset = false) + } + } + } + } + observeEventSticky(EventBus.TTS_PROGRESS) { chapterStart -> + lifecycleScope.launch(IO) { + if (BaseReadAloudService.isPlay()) { + ReadBook.curTextChapter?.let { textChapter -> + ReadBook.durChapterPos = chapterStart + val pageIndex = ReadBook.durPageIndex + val aloudSpanStart = chapterStart - textChapter.getReadLength(pageIndex) + textChapter.getPage(pageIndex) + ?.upPageAloudSpan(aloudSpanStart) + upContent() + } + } + } + } + observeEvent(PreferKey.keepLight) { + upScreenTimeOut() + } + observeEvent(PreferKey.textSelectAble) { + readView.curPage.upSelectAble(it) + } + observeEvent(PreferKey.showBrightnessView) { + readMenu.upBrightnessState() + } + observeEvent>(EventBus.SEARCH_RESULT) { + viewModel.searchResultList = it + } + observeEvent(EventBus.UPDATE_READ_ACTION_BAR) { + readMenu.reset() + } + observeEvent(EventBus.UP_SEEK_BAR) { + readMenu.upSeekBar() + } + } + + private fun upScreenTimeOut() { + val keepLightPrefer = getPrefString(PreferKey.keepLight)?.toInt() ?: 0 + screenTimeOut = keepLightPrefer * 1000L + screenOffTimerStart() + } + + /** + * 重置黑屏时间 + */ + override fun screenOffTimerStart() { + handler.post { + if (screenTimeOut < 0) { + keepScreenOn(true) + return@post + } + val t = screenTimeOut - sysScreenOffTime + if (t > 0) { + keepScreenOn(true) + handler.removeCallbacks(screenOffRunnable) + handler.postDelayed(screenOffRunnable, screenTimeOut) + } else { + keepScreenOn(false) + } + } + } + + companion object { + const val RESULT_DELETED = 100 + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt new file mode 100755 index 000000000..d99e90a29 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -0,0 +1,569 @@ +package io.legado.app.ui.book.read + +import android.app.Application +import android.content.Intent +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isLocalModified +import io.legado.app.help.book.removeType +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.model.localBook.LocalBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.searchContent.SearchResult +import io.legado.app.utils.DocumentUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.mapParallelSafe +import io.legado.app.utils.postEvent +import io.legado.app.utils.toStringArray +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.take +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream + +/** + * 阅读界面数据处理 + */ +class ReadBookViewModel(application: Application) : BaseViewModel(application) { + val permissionDenialLiveData = MutableLiveData() + var isInitFinish = false + var searchContentQuery = "" + var searchResultList: List? = null + var searchResultIndex: Int = 0 + private var changeSourceCoroutine: Coroutine<*>? = null + + init { + AppConfig.detectClickArea() + } + + fun initReadBookConfig(intent: Intent) { + val bookUrl = intent.getStringExtra("bookUrl") + val book = when { + bookUrl.isNullOrEmpty() -> appDb.bookDao.lastReadBook + else -> appDb.bookDao.getBook(bookUrl) + } ?: return + ReadBook.upReadBookConfig(book) + } + + /** + * 初始化 + */ + fun initData(intent: Intent, success: (() -> Unit)? = null) { + execute { + ReadBook.inBookshelf = intent.getBooleanExtra("inBookshelf", true) + ReadBook.chapterChanged = intent.getBooleanExtra("chapterChanged", false) + val bookUrl = intent.getStringExtra("bookUrl") + val book = when { + bookUrl.isNullOrEmpty() -> appDb.bookDao.lastReadBook + else -> appDb.bookDao.getBook(bookUrl) + } ?: ReadBook.book + when { + book != null -> initBook(book) + else -> { + ReadBook.upMsg(context.getString(R.string.no_book)) + AppLog.put("未找到书籍\nbookUrl:$bookUrl") + } + } + }.onSuccess { + success?.invoke() + }.onError { + val msg = "初始化数据失败\n${it.localizedMessage}" + ReadBook.upMsg(msg) + AppLog.put(msg, it) + }.onFinally { + ReadBook.saveRead() + } + } + + private suspend fun initBook(book: Book) { + val isSameBook = ReadBook.book?.bookUrl == book.bookUrl + if (isSameBook) { + ReadBook.upData(book) + } else { + ReadBook.resetData(book) + } + isInitFinish = true + if (!book.isLocal && book.tocUrl.isEmpty() && !loadBookInfo(book)) { + return + } + if (book.isLocal && !checkLocalBookFileExist(book)) { + return + } + if ((ReadBook.chapterSize == 0 || book.isLocalModified()) && !loadChapterListAwait(book)) { + return + } + ReadBook.upMsg(null) + if (!isSameBook) { + ReadBook.loadContent(resetPageOffset = true) + } else { + ReadBook.loadOrUpContent() + } + if (ReadBook.chapterChanged) { + // 有章节跳转不同步阅读进度 + ReadBook.chapterChanged = false + } else if (!(isSameBook && BaseReadAloudService.isRun) && ReadBook.inBookshelf) { + if (AppConfig.syncBookProgressPlus) { + ReadBook.syncProgress({ progress -> ReadBook.callBack?.sureNewProgress(progress) }) + } else { + syncBookProgress(book) + } + } + if (!book.isLocal && ReadBook.bookSource == null) { + autoChangeSource(book.name, book.author) + return + } + } + + private fun checkLocalBookFileExist(book: Book): Boolean { + try { + LocalBook.getBookInputStream(book) + return true + } catch (e: Throwable) { + ReadBook.upMsg("打开本地书籍出错: ${e.localizedMessage}") + if (e is SecurityException || e is FileNotFoundException) { + permissionDenialLiveData.postValue(0) + } + return false + } + } + + /** + * 加载详情页 + */ + private suspend fun loadBookInfo(book: Book): Boolean { + val source = ReadBook.bookSource ?: return true + try { + WebBook.getBookInfoAwait(source, book, canReName = false) + return true + } catch (e: Throwable) { + currentCoroutineContext().ensureActive() + ReadBook.upMsg("详情页出错: ${e.localizedMessage}") + return false + } + } + + /** + * 加载目录 + */ + fun loadChapterList(book: Book) { + execute { + if (loadChapterListAwait(book)) { + ReadBook.upMsg(null) + } + } + } + + private suspend fun loadChapterListAwait(book: Book): Boolean { + if (book.isLocal) { + kotlin.runCatching { + LocalBook.getChapterList(book).let { + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*it.toTypedArray()) + appDb.bookDao.update(book) + ReadBook.onChapterListUpdated(book) + } + return true + }.onFailure { + when (it) { + is SecurityException, is FileNotFoundException -> { + permissionDenialLiveData.postValue(1) + } + + else -> { + AppLog.put("LoadTocError:${it.localizedMessage}", it) + ReadBook.upMsg("LoadTocError:${it.localizedMessage}") + } + } + return false + } + } else { + ReadBook.bookSource?.let { + val oldBook = book.copy() + WebBook.getChapterListAwait(it, book, true) + .onSuccess { cList -> + if (oldBook.bookUrl == book.bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(oldBook.bookUrl) + appDb.bookChapterDao.insert(*cList.toTypedArray()) + ReadBook.onChapterListUpdated(book) + return true + }.onFailure { + currentCoroutineContext().ensureActive() + ReadBook.upMsg(context.getString(R.string.error_load_toc)) + return false + } + } + } + return true + } + + /** + * 同步进度 + */ + fun syncBookProgress( + book: Book, + alertSync: ((progress: BookProgress) -> Unit)? = null + ) { + if (!AppConfig.syncBookProgress) return + execute { + AppWebDav.getBookProgress(book) + }.onError { + AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it) + }.onSuccess { progress -> + progress ?: return@onSuccess + if (progress.durChapterIndex == book.durChapterIndex && progress.durChapterPos == book.durChapterPos) { + return@onSuccess + } + if (progress.durChapterIndex < book.durChapterIndex || + (progress.durChapterIndex == book.durChapterIndex + && progress.durChapterPos < book.durChapterPos) + ) { + alertSync?.invoke(progress) + } else if (progress.durChapterIndex < book.simulatedTotalChapterNum()) { + ReadBook.setProgress(progress) + AppLog.put("自动同步阅读进度成功《${book.name}》 ${progress.durChapterTitle}") + context.toastOnUi("已同步最新阅读进度") + } + } + } + + /** + * 换源 + */ + fun changeTo(book: Book, toc: List) { + changeSourceCoroutine?.cancel() + changeSourceCoroutine = execute { + ReadBook.upMsg(context.getString(R.string.loading)) + ReadBook.book?.migrateTo(book, toc) + book.removeType(BookType.updateError) + ReadBook.book?.delete() + appDb.bookDao.insert(book) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + ReadBook.resetData(book) + ReadBook.upMsg(null) + ReadBook.loadContent(resetPageOffset = true) + }.onError { + AppLog.put("换源失败\n$it", it, true) + ReadBook.upMsg(null) + }.onFinally { + postEvent(EventBus.SOURCE_CHANGED, book.bookUrl) + } + } + + /** + * 自动换源 + */ + private fun autoChangeSource(name: String, author: String) { + if (!AppConfig.autoChangeSource) return + execute { + val sources = appDb.bookSourceDao.allTextEnabledPart + flow { + for (source in sources) { + source.getBookSource()?.let { + emit(it) + } + } + }.onStart { + ReadBook.upMsg(context.getString(R.string.source_auto_changing)) + }.mapParallelSafe(AppConfig.threadCount) { source -> + val book = WebBook.preciseSearchAwait(source, name, author).getOrThrow() + if (book.tocUrl.isEmpty()) { + WebBook.getBookInfoAwait(source, book) + } + val toc = WebBook.getChapterListAwait(source, book).getOrThrow() + val chapter = toc.getOrElse(book.durChapterIndex) { + toc.last() + } + val nextChapter = toc.getOrElse(chapter.index) { + toc.first() + } + WebBook.getContentAwait( + bookSource = source, + book = book, + bookChapter = chapter, + nextChapterUrl = nextChapter.url + ) + book to toc + }.take(1).onEach { (book, toc) -> + changeTo(book, toc) + }.onEmpty { + throw NoStackTraceException("没有合适书源") + }.onCompletion { + ReadBook.upMsg(null) + }.catch { + AppLog.put("自动换源失败\n${it.localizedMessage}", it) + context.toastOnUi("自动换源失败\n${it.localizedMessage}") + }.collect() + } + } + + fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) { + ReadBook.openChapter(index, durChapterPos, success = success) + } + + fun removeFromBookshelf(success: (() -> Unit)?) { + val book = ReadBook.book + Coroutine.async { + book?.delete() + }.onSuccess { + success?.invoke() + } + } + + fun upBookSource(success: (() -> Unit)?) { + execute { + ReadBook.book?.let { book -> + ReadBook.bookSource = appDb.bookSourceDao.getBookSource(book.origin) + } + }.onSuccess { + success?.invoke() + } + } + + fun refreshContentDur(book: Book) { + execute { + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?.let { chapter -> + BookHelp.delContent(book, chapter) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + } + + fun refreshContentAfter(book: Book) { + execute { + appDb.bookChapterDao.getChapterList( + book.bookUrl, + ReadBook.durChapterIndex, + book.totalChapterNum + ).forEach { chapter -> + BookHelp.delContent(book, chapter) + } + ReadBook.loadContent(false) + } + } + + fun refreshContentAll(book: Book) { + execute { + BookHelp.clearCache(book) + ReadBook.loadContent(false) + } + } + + /** + * 保存内容 + */ + fun saveContent(book: Book, content: String) { + execute { + appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?.let { chapter -> + BookHelp.saveText(book, chapter, content) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + } + + /** + * 反转内容 + */ + fun reverseContent(book: Book) { + execute { + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex) + ?: return@execute + val content = BookHelp.getContent(book, chapter) ?: return@execute + val stringBuilder = StringBuilder() + content.toStringArray().forEach { + stringBuilder.insert(0, it) + } + BookHelp.saveText(book, chapter, stringBuilder.toString()) + ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false) + } + } + + /** + * 内容搜索跳转 + */ + fun searchResultPositions( + textChapter: TextChapter, + searchResult: SearchResult + ): Array { + // calculate search result's pageIndex + val pages = textChapter.pages + val content = textChapter.getContent() + val queryLength = searchContentQuery.length + + var count = 0 + var index = content.indexOf(searchContentQuery) + while (count != searchResult.resultCountWithinChapter) { + index = content.indexOf(searchContentQuery, index + queryLength) + count += 1 + } + val contentPosition = index + var pageIndex = 0 + var length = pages[pageIndex].text.length + while (length < contentPosition && pageIndex + 1 < pages.size) { + pageIndex += 1 + length += pages[pageIndex].text.length + } + + // calculate search result's lineIndex + val currentPage = pages[pageIndex] + val curTextLines = currentPage.lines + var lineIndex = 0 + var curLine = curTextLines[lineIndex] + length = length - currentPage.text.length + curLine.text.length + if (curLine.isParagraphEnd) length++ + while (length <= contentPosition && lineIndex + 1 < curTextLines.size) { + lineIndex += 1 + curLine = curTextLines[lineIndex] + length += curLine.text.length + if (curLine.isParagraphEnd) length++ + } + + // charIndex + val currentLine = currentPage.lines[lineIndex] + var curLineLength = currentLine.text.length + if (currentLine.isParagraphEnd) curLineLength++ + length -= curLineLength + + val charIndex = contentPosition - length + var addLine = 0 + var charIndex2 = 0 + // change line + if ((charIndex + queryLength) > curLineLength) { + addLine = 1 + charIndex2 = charIndex + queryLength - curLineLength - 1 + } + // changePage + if ((lineIndex + addLine + 1) > currentPage.lines.size) { + addLine = -1 + charIndex2 = charIndex + queryLength - curLineLength - 1 + } + return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2) + } + + /** + * 翻转删除重复标题 + */ + fun reverseRemoveSameTitle() { + execute { + val book = ReadBook.book ?: return@execute + val textChapter = ReadBook.curTextChapter ?: return@execute + BookHelp.setRemoveSameTitle( + book, textChapter.chapter, !textChapter.sameTitleRemoved + ) + ReadBook.loadContent(ReadBook.durChapterIndex) + } + } + + /** + * 刷新图片 + */ + fun refreshImage(src: String) { + execute { + ReadBook.book?.let { book -> + val vFile = BookHelp.getImage(book, src) + ImageProvider.bitmapLruCache.remove(vFile.absolutePath) + vFile.delete() + } + }.onFinally { + ReadBook.loadContent(false) + } + } + + /** + * 保存图片 + */ + fun saveImage(src: String?, uri: Uri) { + src ?: return + val book = ReadBook.book ?: return + execute { + val image = BookHelp.getImage(book, src) + FileInputStream(image).use { input -> + if (uri.isContentScheme()) { + DocumentFile.fromTreeUri(context, uri)?.let { doc -> + val imageDoc = DocumentUtils.createFileIfNotExist(doc, image.name)!! + context.contentResolver.openOutputStream(imageDoc.uri)!!.use { output -> + input.copyTo(output) + } + } + } else { + val dir = File(uri.path ?: uri.toString()) + val file = FileUtils.createFileIfNotExist(dir, image.name) + FileOutputStream(file).use { output -> + input.copyTo(output) + } + } + } + }.onError { + AppLog.put("保存图片出错\n${it.localizedMessage}", it) + context.toastOnUi("保存图片出错\n${it.localizedMessage}") + } + } + + /** + * 替换规则变化 + */ + fun replaceRuleChanged() { + execute { + ReadBook.book?.let { + ContentProcessor.get(it.name, it.origin).upReplaceRules() + ReadBook.loadContent(resetPageOffset = false) + } + } + } + + fun disableSource() { + execute { + ReadBook.bookSource?.let { + it.enabled = false + appDb.bookSourceDao.update(it) + } + } + } + + override fun onCleared() { + super.onCleared() + if (BaseReadAloudService.isRun && BaseReadAloudService.pause) { + ReadAloud.stop(context) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt new file mode 100755 index 000000000..1ecb20149 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt @@ -0,0 +1,590 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE +import android.view.animation.Animation +import android.widget.FrameLayout +import android.widget.SeekBar +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.isGone +import androidx.core.view.isVisible +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ViewReadMenuBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.source.getSourceType +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.buttonDisabledColor +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.browser.WebViewActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.ConstraintModify +import io.legado.app.utils.activity +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.loadAnimation +import io.legado.app.utils.modifyBegin +import io.legado.app.utils.openUrl +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.startActivity +import io.legado.app.utils.visible +import splitties.views.onClick +import splitties.views.onLongClick + +/** + * 阅读界面菜单 + */ +class ReadMenu @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + var canShowMenu: Boolean = false + private val callBack: CallBack get() = activity as CallBack + private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true) + private var confirmSkipToChapter: Boolean = false + private var isMenuOutAnimating = false + private val menuTopIn: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_top_in) + } + private val menuTopOut: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_top_out) + } + private val menuBottomIn: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_bottom_in) + } + private val menuBottomOut: Animation by lazy { + loadAnimation(context, R.anim.anim_readbook_bottom_out) + } + private val immersiveMenu: Boolean + get() = AppConfig.readBarStyleFollowPage && ReadBookConfig.durConfig.curBgType() == 0 + private var bgColor: Int = if (immersiveMenu) { + kotlin.runCatching { + Color.parseColor(ReadBookConfig.durConfig.curBgStr()) + }.getOrDefault(context.bottomBackground) + } else { + context.bottomBackground + } + private var textColor: Int = if (immersiveMenu) { + ReadBookConfig.durConfig.curTextColor() + } else { + context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + } + + private var bottomBackgroundList: ColorStateList = Selector.colorBuild() + .setDefaultColor(bgColor) + .setPressedColor(ColorUtils.darkenColor(bgColor)) + .create() + private var onMenuOutEnd: (() -> Unit)? = null + private val showBrightnessView + get() = context.getPrefBoolean( + PreferKey.showBrightnessView, + true + ) + private val sourceMenu by lazy { + PopupMenu(context, binding.tvSourceAction).apply { + inflate(R.menu.book_read_source) + setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_login -> callBack.showLogin() + R.id.menu_chapter_pay -> callBack.payAction() + R.id.menu_edit_source -> callBack.openSourceEditActivity() + R.id.menu_disable_source -> callBack.disableSource() + } + true + } + } + } + private val menuInListener = object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + binding.tvSourceAction.text = + ReadBook.bookSource?.bookSourceName ?: context.getString(R.string.book_source) + binding.tvSourceAction.isGone = ReadBook.isLocalBook + callBack.upSystemUiVisibility() + binding.llBrightness.visible(showBrightnessView) + } + + @SuppressLint("RtlHardcoded") + override fun onAnimationEnd(animation: Animation) { + binding.vwMenuBg.setOnClickListener { runMenuOut() } + callBack.upSystemUiVisibility() + if (!LocalConfig.readMenuHelpVersionIsLast) { + callBack.showHelp() + } + } + + override fun onAnimationRepeat(animation: Animation) = Unit + } + private val menuOutListener = object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + isMenuOutAnimating = true + binding.vwMenuBg.setOnClickListener(null) + } + + override fun onAnimationEnd(animation: Animation) { + this@ReadMenu.invisible() + binding.titleBar.invisible() + binding.bottomMenu.invisible() + canShowMenu = false + isMenuOutAnimating = false + onMenuOutEnd?.invoke() + callBack.upSystemUiVisibility() + } + + override fun onAnimationRepeat(animation: Animation) = Unit + } + + init { + initView() + upBrightnessState() + bindEvent() + } + + private fun initView(reset: Boolean = false) = binding.run { + if (AppConfig.isNightTheme) { + fabNightTheme.setImageResource(R.drawable.ic_daytime) + } else { + fabNightTheme.setImageResource(R.drawable.ic_brightness) + } + initAnimation() + if (immersiveMenu) { + val lightTextColor = ColorUtils.withAlpha(ColorUtils.lightenColor(textColor), 0.75f) + titleBar.setTextColor(textColor) + titleBar.setBackgroundColor(bgColor) + titleBar.setColorFilter(textColor) + tvChapterName.setTextColor(lightTextColor) + tvChapterUrl.setTextColor(lightTextColor) + } else if (reset) { + val bgColor = context.primaryColor + val textColor = context.primaryTextColor + titleBar.setTextColor(textColor) + titleBar.setBackgroundColor(bgColor) + titleBar.setColorFilter(textColor) + tvChapterName.setTextColor(textColor) + tvChapterUrl.setTextColor(textColor) + } + val brightnessBackground = GradientDrawable() + brightnessBackground.cornerRadius = 5F.dpToPx() + brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f)) + llBrightness.background = brightnessBackground + if (AppConfig.isEInkMode) { + titleBar.setBackgroundResource(R.drawable.bg_eink_border_bottom) + llBottomBg.setBackgroundResource(R.drawable.bg_eink_border_top) + } else { + llBottomBg.setBackgroundColor(bgColor) + } + fabSearch.backgroundTintList = bottomBackgroundList + fabSearch.setColorFilter(textColor) + fabAutoPage.backgroundTintList = bottomBackgroundList + fabAutoPage.setColorFilter(textColor) + fabReplaceRule.backgroundTintList = bottomBackgroundList + fabReplaceRule.setColorFilter(textColor) + fabNightTheme.backgroundTintList = bottomBackgroundList + fabNightTheme.setColorFilter(textColor) + tvPre.setTextColor(textColor) + tvNext.setTextColor(textColor) + ivCatalog.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvCatalog.setTextColor(textColor) + ivReadAloud.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvReadAloud.setTextColor(textColor) + ivFont.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvFont.setTextColor(textColor) + ivSetting.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvSetting.setTextColor(textColor) + vwBrightnessPosAdjust.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + llBrightness.setOnClickListener(null) + seekBrightness.post { + seekBrightness.progress = AppConfig.readBrightness + } + if (AppConfig.showReadTitleBarAddition) { + titleBarAddition.visible() + } else { + titleBarAddition.gone() + } + upBrightnessVwPos() + /** + * 确保视图不被导航栏遮挡 + */ + applyNavigationBarPadding() + } + + fun reset() { + upColorConfig() + initView(true) + } + + fun refreshMenuColorFilter() { + if (immersiveMenu) { + binding.titleBar.setColorFilter(textColor) + } + } + + private fun upColorConfig() { + bgColor = if (immersiveMenu) { + kotlin.runCatching { + Color.parseColor(ReadBookConfig.durConfig.curBgStr()) + }.getOrDefault(context.bottomBackground) + } else { + context.bottomBackground + } + textColor = if (immersiveMenu) { + ReadBookConfig.durConfig.curTextColor() + } else { + context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + } + bottomBackgroundList = Selector.colorBuild() + .setDefaultColor(bgColor) + .setPressedColor(ColorUtils.darkenColor(bgColor)) + .create() + } + + fun upBrightnessState() { + if (brightnessAuto()) { + binding.ivBrightnessAuto.setColorFilter(context.accentColor) + binding.seekBrightness.isEnabled = false + } else { + binding.ivBrightnessAuto.setColorFilter(context.buttonDisabledColor) + binding.seekBrightness.isEnabled = true + } + setScreenBrightness(AppConfig.readBrightness.toFloat()) + } + + /** + * 设置屏幕亮度 + */ + fun setScreenBrightness(value: Float) { + activity?.run { + var brightness = BRIGHTNESS_OVERRIDE_NONE + if (!brightnessAuto() && value != BRIGHTNESS_OVERRIDE_NONE) { + brightness = value + if (brightness < 1f) brightness = 1f + brightness /= 255f + } + val params = window.attributes + params.screenBrightness = brightness + window.attributes = params + } + } + + fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) { + callBack.onMenuShow() + this.visible() + binding.titleBar.visible() + binding.bottomMenu.visible() + if (anim) { + binding.titleBar.startAnimation(menuTopIn) + binding.bottomMenu.startAnimation(menuBottomIn) + } else { + menuInListener.onAnimationStart(menuBottomIn) + menuInListener.onAnimationEnd(menuBottomIn) + } + } + + fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) { + if (isMenuOutAnimating) { + return + } + callBack.onMenuHide() + this.onMenuOutEnd = onMenuOutEnd + if (this.isVisible) { + if (anim) { + binding.titleBar.startAnimation(menuTopOut) + binding.bottomMenu.startAnimation(menuBottomOut) + } else { + menuOutListener.onAnimationStart(menuBottomOut) + menuOutListener.onAnimationEnd(menuBottomOut) + } + } + } + + private fun brightnessAuto(): Boolean { + return context.getPrefBoolean("brightnessAuto", true) || !showBrightnessView + } + + private fun bindEvent() = binding.run { + vwMenuBg.setOnClickListener { runMenuOut() } + titleBar.toolbar.setOnClickListener { + callBack.openBookInfoActivity() + } + val chapterViewClickListener = OnClickListener { + if (ReadBook.isLocalBook) { + return@OnClickListener + } + if (AppConfig.readUrlInBrowser) { + context.openUrl(tvChapterUrl.text.toString().substringBefore(",{")) + } else { + Coroutine.async { + context.startActivity { + val url = tvChapterUrl.text.toString() + val bookSource = ReadBook.bookSource + putExtra("title", tvChapterName.text) + putExtra("url", url) + putExtra("sourceOrigin", bookSource?.bookSourceUrl) + putExtra("sourceName", bookSource?.bookSourceName) + putExtra("sourceType", bookSource?.getSourceType()) + } + } + } + } + val chapterViewLongClickListener = OnLongClickListener { + if (ReadBook.isLocalBook) { + return@OnLongClickListener true + } + context.alert(R.string.open_fun) { + setMessage(R.string.use_browser_open) + okButton { + AppConfig.readUrlInBrowser = true + } + noButton { + AppConfig.readUrlInBrowser = false + } + } + true + } + tvChapterName.setOnClickListener(chapterViewClickListener) + tvChapterName.setOnLongClickListener(chapterViewLongClickListener) + tvChapterUrl.setOnClickListener(chapterViewClickListener) + tvChapterUrl.setOnLongClickListener(chapterViewLongClickListener) + //书源操作 + tvSourceAction.onClick { + sourceMenu.menu.findItem(R.id.menu_login).isVisible = + !ReadBook.bookSource?.loginUrl.isNullOrEmpty() + sourceMenu.menu.findItem(R.id.menu_chapter_pay).isVisible = + !ReadBook.bookSource?.loginUrl.isNullOrEmpty() + && ReadBook.curTextChapter?.isVip == true + && ReadBook.curTextChapter?.isPay != true + sourceMenu.show() + } + //亮度跟随 + ivBrightnessAuto.setOnClickListener { + context.putPrefBoolean("brightnessAuto", !brightnessAuto()) + upBrightnessState() + } + //亮度调节 + seekBrightness.setOnSeekBarChangeListener(object : SeekBarChangeListener { + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (fromUser) { + setScreenBrightness(progress.toFloat()) + } + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + AppConfig.readBrightness = seekBar.progress + } + + }) + vwBrightnessPosAdjust.setOnClickListener { + AppConfig.brightnessVwPos = !AppConfig.brightnessVwPos + upBrightnessVwPos() + } + //阅读进度 + seekReadPage.setOnSeekBarChangeListener(object : SeekBarChangeListener { + + override fun onStartTrackingTouch(seekBar: SeekBar) { + binding.vwMenuBg.setOnClickListener(null) + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + binding.vwMenuBg.setOnClickListener { runMenuOut() } + when (AppConfig.progressBarBehavior) { + "page" -> ReadBook.skipToPage(seekBar.progress) + "chapter" -> { + if (confirmSkipToChapter) { + callBack.skipToChapter(seekBar.progress) + } else { + context.alert("章节跳转确认", "确定要跳转章节吗?") { + yesButton { + confirmSkipToChapter = true + callBack.skipToChapter(seekBar.progress) + } + noButton { + upSeekBar() + } + onCancelled { + upSeekBar() + } + } + } + } + } + } + + }) + + //搜索 + fabSearch.setOnClickListener { + runMenuOut { + callBack.openSearchActivity(null) + } + } + + //自动翻页 + fabAutoPage.setOnClickListener { + runMenuOut { + callBack.autoPage() + } + } + + //替换 + fabReplaceRule.setOnClickListener { callBack.openReplaceRule() } + + //夜间模式 + fabNightTheme.setOnClickListener { + AppConfig.isNightTheme = !AppConfig.isNightTheme + ThemeConfig.applyDayNight(context) + } + + //上一章 + tvPre.setOnClickListener { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } + + //下一章 + tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) } + + //目录 + llCatalog.setOnClickListener { + runMenuOut { + callBack.openChapterList() + } + } + + //朗读 + llReadAloud.setOnClickListener { + runMenuOut { + callBack.onClickReadAloud() + } + } + llReadAloud.onLongClick { + runMenuOut { + callBack.showReadAloudDialog() + } + } + //界面 + llFont.setOnClickListener { + runMenuOut { + callBack.showReadStyle() + } + } + + //设置 + llSetting.setOnClickListener { + runMenuOut { + callBack.showMoreSetting() + } + } + } + + private fun initAnimation() { + menuTopIn.setAnimationListener(menuInListener) + menuTopOut.setAnimationListener(menuOutListener) + } + + fun upBookView() { + binding.titleBar.title = ReadBook.book?.name + ReadBook.curTextChapter?.let { + binding.tvChapterName.text = it.title + binding.tvChapterName.visible() + if (!ReadBook.isLocalBook) { + binding.tvChapterUrl.text = it.chapter.getAbsoluteURL() + binding.tvChapterUrl.visible() + } else { + binding.tvChapterUrl.gone() + } + upSeekBar() + binding.tvPre.isEnabled = ReadBook.durChapterIndex != 0 + binding.tvNext.isEnabled = ReadBook.durChapterIndex != ReadBook.simulatedChapterSize - 1 + } ?: let { + binding.tvChapterName.gone() + binding.tvChapterUrl.gone() + } + } + + fun upSeekBar() { + binding.seekReadPage.apply { + when (AppConfig.progressBarBehavior) { + "page" -> { + ReadBook.curTextChapter?.let { + max = it.pageSize.minus(1) + progress = ReadBook.durPageIndex + } + } + + "chapter" -> { + max = ReadBook.simulatedChapterSize - 1 + progress = ReadBook.durChapterIndex + } + } + } + } + + fun setSeekPage(seek: Int) { + binding.seekReadPage.progress = seek + } + + fun setAutoPage(autoPage: Boolean) = binding.run { + if (autoPage) { + fabAutoPage.setImageResource(R.drawable.ic_auto_page_stop) + fabAutoPage.contentDescription = context.getString(R.string.auto_next_page_stop) + } else { + fabAutoPage.setImageResource(R.drawable.ic_auto_page) + fabAutoPage.contentDescription = context.getString(R.string.auto_next_page) + } + fabAutoPage.setColorFilter(textColor) + } + + private fun upBrightnessVwPos() { + if (AppConfig.brightnessVwPos) { + binding.root.modifyBegin() + .clear(R.id.ll_brightness, ConstraintModify.Anchor.LEFT) + .rightToRightOf(R.id.ll_brightness, R.id.vw_menu_root) + .commit() + } else { + binding.root.modifyBegin() + .clear(R.id.ll_brightness, ConstraintModify.Anchor.RIGHT) + .leftToLeftOf(R.id.ll_brightness, R.id.vw_menu_root) + .commit() + } + } + + interface CallBack { + fun autoPage() + fun openReplaceRule() + fun openChapterList() + fun openSearchActivity(searchWord: String?) + fun openSourceEditActivity() + fun openBookInfoActivity() + fun showReadStyle() + fun showMoreSetting() + fun showReadAloudDialog() + fun upSystemUiVisibility() + fun onClickReadAloud() + fun showHelp() + fun showLogin() + fun payAction() + fun disableSource() + fun skipToChapter(index: Int) + fun onMenuShow() + fun onMenuHide() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt new file mode 100755 index 000000000..07c38ecc7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/SearchMenu.kt @@ -0,0 +1,234 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.animation.Animation +import android.widget.FrameLayout +import androidx.core.view.isVisible +import io.legado.app.R +import io.legado.app.databinding.ViewSearchMenuBinding +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.searchContent.SearchResult +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.activity +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.invisible +import io.legado.app.utils.loadAnimation +import io.legado.app.utils.visible + +/** + * 搜索界面菜单 + */ +class SearchMenu @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + private val callBack: CallBack get() = activity as CallBack + private val binding = ViewSearchMenuBinding.inflate(LayoutInflater.from(context), this, true) + + private val menuBottomIn: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_in) + private val menuBottomOut: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_out) + private val bgColor: Int = context.bottomBackground + private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + private val bottomBackgroundList: ColorStateList = + Selector.colorBuild().setDefaultColor(bgColor) + .setPressedColor(ColorUtils.darkenColor(bgColor)).create() + private var onMenuOutEnd: (() -> Unit)? = null + private var isMenuOutAnimating = false + + private val searchResultList: MutableList = mutableListOf() + private var currentSearchResultIndex: Int = -1 + private var lastSearchResultIndex: Int = -1 + private val hasSearchResult: Boolean + get() = searchResultList.isNotEmpty() + val selectedSearchResult: SearchResult? + get() = searchResultList.getOrNull(currentSearchResultIndex) + val previousSearchResult: SearchResult? + get() = searchResultList.getOrNull(lastSearchResultIndex) + val bottomMenuVisible get() = isVisible && binding.llBottomMenu.isVisible + + init { + initAnimation() + initView() + bindEvent() + updateSearchInfo() + } + + fun upSearchResultList(resultList: List) { + searchResultList.clear() + searchResultList.addAll(resultList) + updateSearchInfo() + } + + private fun initView() = binding.run { + llSearchBaseInfo.setBackgroundColor(bgColor) + tvCurrentSearchInfo.setTextColor(bottomBackgroundList) + llBottomBg.setBackgroundColor(bgColor) + fabLeft.backgroundTintList = bottomBackgroundList + fabLeft.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + fabRight.backgroundTintList = bottomBackgroundList + fabRight.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvMainMenu.setTextColor(textColor) + tvSearchResults.setTextColor(textColor) + tvSearchExit.setTextColor(textColor) + ivMainMenu.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + ivSearchResults.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + ivSearchExit.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + ivSearchContentUp.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + ivSearchContentDown.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvCurrentSearchInfo.setTextColor(textColor) + applyNavigationBarPadding() + } + + + fun runMenuIn() { + this.visible() + binding.llBottomMenu.visible() + binding.vwMenuBg.visible() + binding.llBottomMenu.startAnimation(menuBottomIn) + } + + fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) { + if (isMenuOutAnimating) { + return + } + this.onMenuOutEnd = onMenuOutEnd + if (this.isVisible) { + binding.llBottomMenu.startAnimation(menuBottomOut) + } + } + + @SuppressLint("SetTextI18n") + fun updateSearchInfo() { + ReadBook.curTextChapter?.let { + binding.tvCurrentSearchInfo.text = + """${context.getString(R.string.search_content_size)}: ${searchResultList.size} / 当前章节: ${it.title}""" + } + } + + fun updateSearchResultIndex(updateIndex: Int) { + lastSearchResultIndex = currentSearchResultIndex + currentSearchResultIndex = when { + updateIndex < 0 -> 0 + updateIndex >= searchResultList.size -> searchResultList.size - 1 + else -> updateIndex + } + } + + private fun bindEvent() = binding.run { + //搜索结果 + llSearchResults.setOnClickListener { + runMenuOut { + callBack.openSearchActivity(selectedSearchResult?.query) + } + } + + //主菜单 + llMainMenu.setOnClickListener { + runMenuOut { + callBack.cancelSelect() + callBack.showMenuBar() + this@SearchMenu.invisible() + } + } + + //退出 + llSearchExit.setOnClickListener { + runMenuOut { + callBack.exitSearchMenu() + } + } + + fabLeft.setOnClickListener { + updateSearchResultIndex(currentSearchResultIndex - 1) + callBack.navigateToSearch( + searchResultList[currentSearchResultIndex], + currentSearchResultIndex + ) + } + + ivSearchContentUp.setOnClickListener { + updateSearchResultIndex(currentSearchResultIndex - 1) + callBack.navigateToSearch( + searchResultList[currentSearchResultIndex], + currentSearchResultIndex + ) + } + + ivSearchContentDown.setOnClickListener { + updateSearchResultIndex(currentSearchResultIndex + 1) + callBack.navigateToSearch( + searchResultList[currentSearchResultIndex], + currentSearchResultIndex + ) + } + + fabRight.setOnClickListener { + updateSearchResultIndex(currentSearchResultIndex + 1) + callBack.navigateToSearch( + searchResultList[currentSearchResultIndex], + currentSearchResultIndex + ) + } + } + + private fun initAnimation() { + //显示菜单 + menuBottomIn.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + callBack.upSystemUiVisibility() + binding.fabLeft.visible(hasSearchResult) + binding.fabRight.visible(hasSearchResult) + } + + @SuppressLint("RtlHardcoded") + override fun onAnimationEnd(animation: Animation) { + binding.vwMenuBg.setOnClickListener { runMenuOut() } + callBack.upSystemUiVisibility() + } + + override fun onAnimationRepeat(animation: Animation) = Unit + }) + + //隐藏菜单 + menuBottomOut.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + isMenuOutAnimating = true + binding.vwMenuBg.setOnClickListener(null) + } + + override fun onAnimationEnd(animation: Animation) { + isMenuOutAnimating = false + binding.llBottomMenu.invisible() + binding.vwMenuBg.invisible() + binding.vwMenuBg.setOnClickListener { runMenuOut() } + + onMenuOutEnd?.invoke() + callBack.upSystemUiVisibility() + } + + override fun onAnimationRepeat(animation: Animation) = Unit + }) + } + + interface CallBack { + var isShowingSearchResult: Boolean + fun openSearchActivity(searchWord: String?) + fun showSearchSetting() + fun upSystemUiVisibility() + fun exitSearchMenu() + fun showMenuBar() + fun navigateToSearch(searchResult: SearchResult, index: Int) + fun onMenuShow() + fun onMenuHide() + fun cancelSelect() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt new file mode 100755 index 000000000..f63dd4e24 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt @@ -0,0 +1,295 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.content.pm.ResolveInfo +import android.net.Uri +import android.os.Build +import android.view.Gravity +import android.view.LayoutInflater +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import androidx.annotation.RequiresApi +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.core.view.isVisible +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ItemTextBinding +import io.legado.app.databinding.PopupActionMenuBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.gone +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.sendToClip +import io.legado.app.utils.share +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.visible + +@SuppressLint("RestrictedApi") +class TextActionMenu(private val context: Context, private val callBack: CallBack) : + PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) { + + private val binding = PopupActionMenuBinding.inflate(LayoutInflater.from(context)) + private val adapter = Adapter(context).apply { + setHasStableIds(true) + } + private val menuItems: List + private val visibleMenuItems = arrayListOf() + private val moreMenuItems = arrayListOf() + private val expandTextMenu get() = context.getPrefBoolean(PreferKey.expandTextMenu) + + init { + @SuppressLint("InflateParams") + contentView = binding.root + + isTouchable = true + isOutsideTouchable = false + isFocusable = false + + val myMenu = MenuBuilder(context) + val otherMenu = MenuBuilder(context) + SupportMenuInflater(context).inflate(R.menu.content_select_action, myMenu) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + onInitializeMenu(otherMenu) + } + menuItems = myMenu.visibleItems + otherMenu.visibleItems + visibleMenuItems.addAll(menuItems.subList(0, 5)) + moreMenuItems.addAll(menuItems.subList(5, menuItems.size)) + binding.recyclerView.adapter = adapter + binding.recyclerViewMore.adapter = adapter + setOnDismissListener { + if (!context.getPrefBoolean(PreferKey.expandTextMenu)) { + binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert) + binding.recyclerViewMore.gone() + adapter.setItems(visibleMenuItems) + binding.recyclerView.visible() + } + } + binding.ivMenuMore.setOnClickListener { + if (binding.recyclerView.isVisible) { + binding.ivMenuMore.setImageResource(R.drawable.ic_arrow_back) + adapter.setItems(moreMenuItems) + binding.recyclerView.gone() + binding.recyclerViewMore.visible() + } else { + binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert) + binding.recyclerViewMore.gone() + adapter.setItems(visibleMenuItems) + binding.recyclerView.visible() + } + } + upMenu() + } + + fun upMenu() { + if (expandTextMenu) { + adapter.setItems(menuItems) + binding.ivMenuMore.gone() + } else { + adapter.setItems(visibleMenuItems) + binding.ivMenuMore.visible() + } + } + + fun show( + view: View, + windowHeight: Int, + startX: Int, + startTopY: Int, + startBottomY: Int, + endX: Int, + endBottomY: Int + ) { + if (expandTextMenu) { + when { + startTopY > 500 -> { + showAtLocation( + view, + Gravity.BOTTOM or Gravity.START, + startX, + windowHeight - startTopY + ) + } + + endBottomY - startBottomY > 500 -> { + showAtLocation(view, Gravity.TOP or Gravity.START, startX, startBottomY) + } + + else -> { + showAtLocation(view, Gravity.TOP or Gravity.START, endX, endBottomY) + } + } + } else { + contentView.measure( + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED, + ) + val popupHeight = contentView.measuredHeight + when { + startBottomY > 500 -> { + showAtLocation( + view, + Gravity.TOP or Gravity.START, + startX, + startTopY - popupHeight + ) + } + + endBottomY - startBottomY > 500 -> { + showAtLocation( + view, + Gravity.TOP or Gravity.START, + startX, + startBottomY + ) + } + + else -> { + showAtLocation( + view, + Gravity.TOP or Gravity.START, + endX, + endBottomY + ) + } + } + } + } + + inner class Adapter(context: Context) : + RecyclerAdapter(context) { + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getViewBinding(parent: ViewGroup): ItemTextBinding { + return ItemTextBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemTextBinding, + item: MenuItemImpl, + payloads: MutableList + ) { + with(binding) { + textView.text = item.title + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemTextBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (!callBack.onMenuItemSelected(it.itemId)) { + onMenuItemSelected(it) + } + } + callBack.onMenuActionFinally() + } + holder.itemView.setOnLongClickListener { + if (AppConfig.contentSelectSpeakMod == 0) { + AppConfig.contentSelectSpeakMod = 1 + context.toastOnUi("切换为从选择的地方开始一直朗读") + } else { + AppConfig.contentSelectSpeakMod = 0 + context.toastOnUi("切换为朗读选择内容") + } + true + } + } + } + + private fun onMenuItemSelected(item: MenuItemImpl) { + when (item.itemId) { + R.id.menu_copy -> context.sendToClip(callBack.selectedText) + R.id.menu_share_str -> context.share(callBack.selectedText) + R.id.menu_browser -> { + kotlin.runCatching { + val intent = if (callBack.selectedText.isAbsUrl()) { + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(callBack.selectedText) + } + } else { + Intent(Intent.ACTION_WEB_SEARCH).apply { + putExtra(SearchManager.QUERY, callBack.selectedText) + } + } + context.startActivity(intent) + }.onFailure { + it.printOnDebug() + context.toastOnUi(it.localizedMessage ?: "ERROR") + } + } + + else -> item.intent?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + kotlin.runCatching { + it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText) + context.startActivity(it) + }.onFailure { e -> + AppLog.put("执行文本菜单操作出错\n$e", e, true) + } + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun createProcessTextIntent(): Intent { + return Intent() + .setAction(Intent.ACTION_PROCESS_TEXT) + .setType("text/plain") + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getSupportedActivities(): List { + return context.packageManager + .queryIntentActivities(createProcessTextIntent(), 0) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun createProcessTextIntentForResolveInfo(info: ResolveInfo): Intent { + return createProcessTextIntent() + .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) + .setClassName(info.activityInfo.packageName, info.activityInfo.name) + } + + /** + * Start with a menu Item order value that is high enough + * so that your "PROCESS_TEXT" menu items appear after the + * standard selection menu items like Cut, Copy, Paste. + */ + @RequiresApi(Build.VERSION_CODES.M) + private fun onInitializeMenu(menu: Menu) { + kotlin.runCatching { + var menuItemOrder = 100 + for (resolveInfo in getSupportedActivities()) { + menu.add( + Menu.NONE, Menu.NONE, + menuItemOrder++, resolveInfo.loadLabel(context.packageManager) + ).intent = createProcessTextIntentForResolveInfo(resolveInfo) + } + }.onFailure { + context.toastOnUi("获取文字操作菜单出错:${it.localizedMessage}") + } + } + + interface CallBack { + val selectedText: String + + fun onMenuItemSelected(itemId: Int): Boolean + + fun onMenuActionFinally() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt new file mode 100755 index 000000000..6683a4422 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt @@ -0,0 +1,131 @@ +package io.legado.app.ui.book.read.config + +import android.content.DialogInterface +import android.graphics.PorterDuff +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.SeekBar +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogAutoReadBinding +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.book.read.BaseReadBookActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.viewbindingdelegate.viewBinding +import java.util.Locale + + +class AutoReadDialog : BaseDialogFragment(R.layout.dialog_auto_read) { + + private val binding by viewBinding(DialogAutoReadBinding::bind) + private val callBack: CallBack? get() = activity as? CallBack + + override fun onStart() { + super.onStart() + dialog?.window?.run { + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setBackgroundDrawableResource(R.color.background) + decorView.setPadding(0, 0, 0, 0) + val attr = attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + attributes = attr + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { + val bottomDialog = (activity as ReadBookActivity).bottomDialog++ + if (bottomDialog > 0) { + dismiss() + return@run + } + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + root.setBackgroundColor(bg) + tvReadSpeedTitle.setTextColor(textColor) + tvReadSpeed.setTextColor(textColor) + ivCatalog.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvCatalog.setTextColor(textColor) + ivMainMenu.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvMainMenu.setTextColor(textColor) + ivAutoPageStop.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvAutoPageStop.setTextColor(textColor) + ivSetting.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + tvSetting.setTextColor(textColor) + initOnChange() + initData() + initEvent() + } + + private fun initData() { + val speed = if (ReadBookConfig.autoReadSpeed < 1) 1 else ReadBookConfig.autoReadSpeed + binding.tvReadSpeed.text = String.format(Locale.ROOT, "%ds", speed) + binding.seekAutoRead.progress = speed + } + + private fun initOnChange() { + binding.seekAutoRead.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + val speed = if (progress < 1) 1 else progress + binding.tvReadSpeed.text = String.format(Locale.ROOT, "%ds", speed) + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + ReadBookConfig.autoReadSpeed = + if (binding.seekAutoRead.progress < 1) 1 else binding.seekAutoRead.progress + upTtsSpeechRate() + } + }) + } + + private fun initEvent() { + binding.llMainMenu.setOnClickListener { + callBack?.showMenuBar() + dismissAllowingStateLoss() + } + binding.llSetting.setOnClickListener { + (activity as BaseReadBookActivity).showPageAnimConfig { + (activity as ReadBookActivity).upPageAnim() + ReadBook.loadContent(false) + } + } + binding.llCatalog.setOnClickListener { callBack?.openChapterList() } + binding.llAutoPageStop.setOnClickListener { + callBack?.autoPageStop() + binding.llAutoPageStop.post { + dismissAllowingStateLoss() + } + } + } + + private fun upTtsSpeechRate() { + ReadAloud.upTtsSpeechRate(requireContext()) + if (!BaseReadAloudService.pause) { + ReadAloud.pause(requireContext()) + ReadAloud.resume(requireContext()) + } + } + + interface CallBack { + fun showMenuBar() + fun openChapterList() + fun autoPageStop() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt new file mode 100755 index 000000000..0765f4d0e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgAdapter.kt @@ -0,0 +1,49 @@ +package io.legado.app.ui.book.read.config + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.EventBus +import io.legado.app.databinding.ItemBgImageBinding +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.glide.ImageLoader +import io.legado.app.utils.postEvent +import java.io.File + +class BgAdapter(context: Context, val textColor: Int) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBgImageBinding { + return ItemBgImageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBgImageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + ImageLoader.load( + context, + context.assets.open("bg${File.separator}$item").readBytes() + ) + .centerCrop() + .into(ivBg) + tvName.setTextColor(textColor) + tvName.text = item.substringBeforeLast(".") + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBgImageBinding) { + holder.itemView.apply { + this.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + ReadBookConfig.durConfig.setCurBg(1, it) + postEvent(EventBus.UP_CONFIG, arrayListOf(1)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt new file mode 100755 index 000000000..c59f2a015 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -0,0 +1,400 @@ +package io.legado.app.ui.book.read.config + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.graphics.PorterDuff +import android.net.Uri +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.SeekBar +import androidx.appcompat.widget.TooltipCompat +import androidx.core.graphics.toColorInt +import androidx.core.view.isGone +import com.jaredrummler.android.colorpicker.ColorPickerDialog +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogReadBgTextBinding +import io.legado.app.databinding.ItemBgImageBinding +import io.legado.app.help.DefaultData +import io.legado.app.help.book.isImage +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.getSecondaryTextColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.compress.ZipUtils +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.createFileReplace +import io.legado.app.utils.createFolderReplace +import io.legado.app.utils.delete +import io.legado.app.utils.externalCache +import io.legado.app.utils.externalFiles +import io.legado.app.utils.find +import io.legado.app.utils.getFile +import io.legado.app.utils.inputStream +import io.legado.app.utils.longToast +import io.legado.app.utils.openInputStream +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.outputStream +import io.legado.app.utils.postEvent +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.readBytes +import io.legado.app.utils.readUri +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.init.appCtx +import java.io.File +import java.io.FileOutputStream + +class BgTextConfigDialog : BaseDialogFragment(R.layout.dialog_read_bg_text) { + + companion object { + const val TEXT_COLOR = 121 + const val BG_COLOR = 122 + } + + private val binding by viewBinding(DialogReadBgTextBinding::bind) + private val configFileName = "readConfig.zip" + private val adapter by lazy { BgAdapter(requireContext(), secondaryTextColor) } + private var primaryTextColor = 0 + private var secondaryTextColor = 0 + private val importFormNet = "网络导入" + private val selectBgImage = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + setBgFromUri(uri) + } + } + private val selectExportDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + exportConfig(uri) + } + } + private val selectImportDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + if (uri.path == "/$importFormNet") { + importNetConfigAlert() + } else { + importConfig(uri) + } + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.run { + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setBackgroundDrawableResource(R.color.background) + decorView.setPadding(0, 0, 0, 0) + val attr = attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + attributes = attr + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + (activity as ReadBookActivity).bottomDialog++ + initView() + initData() + initEvent() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + ReadBookConfig.save() + (activity as ReadBookActivity).bottomDialog-- + } + + private fun initView() = binding.run { + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + primaryTextColor = requireContext().getPrimaryTextColor(isLight) + secondaryTextColor = requireContext().getSecondaryTextColor(isLight) + rootView.setBackgroundColor(bg) + tvNameTitle.setTextColor(primaryTextColor) + tvName.setTextColor(secondaryTextColor) + ivEdit.setColorFilter(secondaryTextColor, PorterDuff.Mode.SRC_IN) + tvRestore.setTextColor(primaryTextColor) + swDarkStatusIcon.setTextColor(primaryTextColor) + swUnderline.setTextColor(primaryTextColor) + ivImport.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN) + ivExport.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN) + ivDelete.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN) + tvBgAlpha.setTextColor(primaryTextColor) + tvBgImage.setTextColor(primaryTextColor) + swUnderline.isGone = ReadBook.book?.isImage == true + recyclerView.adapter = adapter + adapter.addHeaderView { + ItemBgImageBinding.inflate(layoutInflater, it, false).apply { + tvName.setTextColor(secondaryTextColor) + tvName.text = getString(R.string.select_image) + ivBg.setImageResource(R.drawable.ic_image) + ivBg.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN) + root.setOnClickListener { + selectBgImage.launch { + mode = HandleFileContract.IMAGE + } + } + } + } + requireContext().assets.list("bg")?.let { + adapter.setItems(it.toList()) + } + } + + @SuppressLint("InflateParams") + private fun initData() = with(ReadBookConfig.durConfig) { + binding.tvName.text = name.ifBlank { "文字" } + binding.swDarkStatusIcon.isChecked = curStatusIconDark() + binding.swUnderline.isChecked = underline + binding.sbBgAlpha.progress = bgAlpha + } + + @SuppressLint("InflateParams") + private fun initEvent() = with(ReadBookConfig.durConfig) { + binding.ivEdit.setOnClickListener { + alert(R.string.style_name) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "name" + editView.setText(ReadBookConfig.durConfig.name) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + binding.tvName.text = it + ReadBookConfig.durConfig.name = it + } + } + cancelButton() + } + } + binding.tvRestore.setOnClickListener { + val defaultConfigs = DefaultData.readConfigs + val layoutNames = defaultConfigs.map { it.name } + context?.selector("选择预设布局", layoutNames) { _, i -> + if (i >= 0) { + ReadBookConfig.durConfig = defaultConfigs[i].copy() + initData() + postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) + } + } + } + binding.swDarkStatusIcon.setOnCheckedChangeListener { _, isChecked -> + setCurStatusIconDark(isChecked) + (activity as? ReadBookActivity)?.upSystemUiVisibility() + } + binding.swUnderline.setOnCheckedChangeListener { _, isChecked -> + underline = isChecked + postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11)) + } + binding.tvTextColor.setOnClickListener { + ColorPickerDialog.newBuilder() + .setColor(curTextColor()) + .setShowAlphaSlider(false) + .setDialogType(ColorPickerDialog.TYPE_CUSTOM) + .setDialogId(TEXT_COLOR) + .show(requireActivity()) + } + binding.tvBgColor.setOnClickListener { + val bgColor = + if (curBgType() == 0) curBgStr().toColorInt() + else "#015A86".toColorInt() + ColorPickerDialog.newBuilder() + .setColor(bgColor) + .setShowAlphaSlider(false) + .setDialogType(ColorPickerDialog.TYPE_CUSTOM) + .setDialogId(BG_COLOR) + .show(requireActivity()) + } + binding.tvBgColor.apply { + TooltipCompat.setTooltipText(this, text) + } + binding.ivImport.setOnClickListener { + selectImportDoc.launch { + mode = HandleFileContract.FILE + title = getString(R.string.import_str) + allowExtensions = arrayOf("zip") + otherActions = arrayListOf(SelectItem(importFormNet, -1)) + } + } + binding.ivExport.setOnClickListener { + selectExportDir.launch { + title = getString(R.string.export_str) + } + } + binding.ivDelete.setOnClickListener { + if (ReadBookConfig.deleteDur()) { + postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) + dismissAllowingStateLoss() + } else { + toastOnUi("数量已是最少,不能删除.") + } + } + binding.sbBgAlpha.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + ReadBookConfig.bgAlpha = progress + postEvent(EventBus.UP_CONFIG, arrayListOf(3)) + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + postEvent(EventBus.UP_CONFIG, arrayListOf(3)) + } + }) + } + + private fun exportConfig(uri: Uri) { + val exportFileName = if (ReadBookConfig.config.name.isBlank()) { + configFileName + } else { + "${ReadBookConfig.config.name}.zip" + } + execute { + val exportFiles = arrayListOf() + val configDir = requireContext().externalCache.getFile("readConfig") + configDir.createFolderReplace() + val configFile = configDir.getFile("readConfig.json") + configFile.createFileReplace() + val config = ReadBookConfig.getExportConfig() + val fontPath = ReadBookConfig.textFont + if (fontPath.isNotEmpty()) { + val fontDoc = FileDoc.fromFile(fontPath) + val fontName = fontDoc.name + val fontInputStream = fontDoc.openInputStream().getOrNull() + fontInputStream?.use { + val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName) + fontExportFile.outputStream().use { out -> + it.copyTo(out) + } + config.textFont = fontName + exportFiles.add(fontExportFile) + } + } + configFile.writeText(GSON.toJson(config)) + exportFiles.add(configFile) + repeat(3) { + val path = ReadBookConfig.durConfig.getBgPath(it) ?: return@repeat + val bgExportFile = copyBgImage(path, configDir) ?: return@repeat + exportFiles.add(bgExportFile) + } + val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName) + if (ZipUtils.zipFiles(exportFiles, File(configZipPath))) { + val exportDir = FileDoc.fromDir(uri) + exportDir.find(exportFileName)?.delete() + val exportFileDoc = exportDir.createFileIfNotExist(exportFileName) + exportFileDoc.openOutputStream().getOrThrow().use { out -> + File(configZipPath).inputStream().use { + it.copyTo(out) + } + } + } + }.onSuccess { + toastOnUi("导出成功, 文件名为 $exportFileName") + }.onError { + it.printOnDebug() + AppLog.put("导出失败:${it.localizedMessage}", it) + longToast("导出失败:${it.localizedMessage}") + } + } + + private fun copyBgImage(path: String, configDir: File): File? { + val bgName = FileUtils.getName(path) + val bgFile = File(path) + if (bgFile.exists()) { + val bgExportFile = File(FileUtils.getPath(configDir, bgName)) + if (!bgExportFile.exists()) { + bgFile.copyTo(bgExportFile) + return bgExportFile + } + } + return null + } + + @SuppressLint("InflateParams") + private fun importNetConfigAlert() { + alert("输入地址") { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { url -> + importNetConfig(url) + } + } + cancelButton() + } + } + + private fun importNetConfig(url: String) { + execute { + okHttpClient.newCallResponseBody { + url(url) + }.bytes().let { + importConfig(it) + } + }.onError { + longToast(it.stackTraceStr) + } + } + + private fun importConfig(uri: Uri) { + execute { + importConfig(uri.readBytes(requireContext())) + }.onError { + it.printOnDebug() + longToast("导入失败:${it.localizedMessage}") + } + } + + private fun importConfig(byteArray: ByteArray) { + execute { + ReadBookConfig.import(byteArray) + }.onSuccess { + ReadBookConfig.durConfig = it + postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) + toastOnUi("导入成功") + }.onError { + it.printOnDebug() + longToast("导入失败:${it.localizedMessage}") + } + } + + private fun setBgFromUri(uri: Uri) { + readUri(uri) { fileDoc, inputStream -> + kotlin.runCatching { + var file = requireContext().externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val fileName = uri.inputStream(requireContext()).getOrThrow().use { + MD5Utils.md5Encode(it) + ".$suffix" + } + file = FileUtils.createFileIfNotExist(file, "bg", fileName) + FileOutputStream(file).use { outputStream -> + inputStream.copyTo(outputStream) + } + ReadBookConfig.durConfig.setCurBg(2, fileName) + postEvent(EventBus.UP_CONFIG, arrayListOf(1)) + }.onFailure { + appCtx.toastOnUi(it.localizedMessage) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt new file mode 100755 index 000000000..fabe3455c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt @@ -0,0 +1,53 @@ +package io.legado.app.ui.book.read.config + +import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.AttributeSet +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.widget.text.StrokeTextView + + +class ChineseConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) { + + private val spannableString = SpannableString("简/繁") + private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor) + private var onChanged: (() -> Unit)? = null + + init { + text = spannableString + if (!isInEditMode) { + upUi(AppConfig.chineseConverterType) + } + setOnClickListener { + selectType() + } + } + + private fun upUi(type: Int) { + spannableString.removeSpan(enabledSpan) + when (type) { + 1 -> spannableString.setSpan(enabledSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 2 -> spannableString.setSpan(enabledSpan, 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + text = spannableString + } + + private fun selectType() { + context.alert(titleResource = R.string.chinese_converter) { + items(context.resources.getStringArray(R.array.chinese_mode).toList()) { _, i -> + AppConfig.chineseConverterType = i + upUi(i) + onChanged?.invoke() + } + } + } + + fun onChanged(unit: () -> Unit) { + onChanged = unit + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt new file mode 100755 index 000000000..6f95ecf06 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt @@ -0,0 +1,150 @@ +package io.legado.app.ui.book.read.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogClickActionConfigBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 点击区域设置 + */ +class ClickActionConfigDialog : BaseDialogFragment(R.layout.dialog_click_action_config) { + private val binding by viewBinding(DialogClickActionConfigBinding::bind) + private val actions by lazy { + linkedMapOf( + Pair(-1, getString(R.string.non_action)), + Pair(0, getString(R.string.menu)), + Pair(1, getString(R.string.next_page)), + Pair(2, getString(R.string.prev_page)), + Pair(3, getString(R.string.next_chapter)), + Pair(4, getString(R.string.previous_chapter)), + Pair(5, getString(R.string.read_aloud_prev_paragraph)), + Pair(6, getString(R.string.read_aloud_next_paragraph)), + Pair(7, getString(R.string.bookmark_add)), + Pair(8, getString(R.string.edit_content)), + Pair(9, getString(R.string.replace_state_change)), + Pair(10, getString(R.string.chapter_list)), + Pair(11, getString(R.string.search_content)), + Pair(12, getString(R.string.sync_book_progress_t)), + Pair(13, getString(R.string.read_aloud_pause_resume)) + ) + } + + override fun onStart() { + super.onStart() + dialog?.window?.run { + setBackgroundDrawableResource(R.color.transparent) + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + (activity as ReadBookActivity).bottomDialog++ + view.setBackgroundColor(getCompatColor(R.color.translucent)) + initData() + initViewEvent() + } + + private fun initData() = binding.run { + tvTopLeft.text = actions[AppConfig.clickActionTL] + tvTopCenter.text = actions[AppConfig.clickActionTC] + tvTopRight.text = actions[AppConfig.clickActionTR] + tvMiddleLeft.text = actions[AppConfig.clickActionML] + tvMiddleCenter.text = actions[AppConfig.clickActionMC] + tvMiddleRight.text = actions[AppConfig.clickActionMR] + tvBottomLeft.text = actions[AppConfig.clickActionBL] + tvBottomCenter.text = actions[AppConfig.clickActionBC] + tvBottomRight.text = actions[AppConfig.clickActionBR] + } + + private fun initViewEvent() { + binding.ivClose.setOnClickListener { + dismissAllowingStateLoss() + } + binding.tvTopLeft.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionTL, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvTopCenter.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionTC, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvTopRight.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionTR, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvMiddleLeft.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionML, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvMiddleCenter.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionMC, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvMiddleRight.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionMR, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvBottomLeft.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionBL, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvBottomCenter.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionBC, action) + (it as? TextView)?.text = actions[action] + } + } + binding.tvBottomRight.setOnClickListener { + selectAction { action -> + putPrefInt(PreferKey.clickActionBR, action) + (it as? TextView)?.text = actions[action] + } + } + } + + private fun selectAction(success: (action: Int) -> Unit) { + context?.selector( + getString(R.string.select_action), + actions.values.toList() + ) { _, index -> + success.invoke(actions.keys.toList()[index]) + } + } + + override fun onDestroy() { + super.onDestroy() + AppConfig.detectClickArea() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditDialog.kt new file mode 100755 index 000000000..3be519462 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditDialog.kt @@ -0,0 +1,139 @@ +package io.legado.app.ui.book.read.config + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.entities.HttpTTS +import io.legado.app.databinding.DialogHttpTtsEditBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.widget.code.addJsPattern +import io.legado.app.ui.widget.code.addJsonPattern +import io.legado.app.ui.widget.code.addLegadoPattern +import io.legado.app.utils.GSON +import io.legado.app.utils.applyTint +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class HttpTtsEditDialog() : BaseDialogFragment(R.layout.dialog_http_tts_edit, true), + Toolbar.OnMenuItemClickListener { + + constructor(id: Long) : this() { + arguments = Bundle().apply { + putLong("id", id) + } + } + + private val binding by viewBinding(DialogHttpTtsEditBinding::bind) + private val viewModel by viewModels() + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.tvUrl.run { + addLegadoPattern() + addJsonPattern() + addJsPattern() + } + binding.tvLoginUrl.run { + addLegadoPattern() + addJsonPattern() + addJsPattern() + } + binding.tvLoginUi.addJsonPattern() + binding.tvLoginCheckJs.addJsPattern() + binding.tvHeaders.run { + addLegadoPattern() + addJsonPattern() + addJsPattern() + } + viewModel.initData(arguments) { + initView(httpTTS = it) + } + initMenu() + } + + fun initMenu() { + binding.toolBar.inflateMenu(R.menu.speak_engine_edit) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + } + + fun initView(httpTTS: HttpTTS) { + binding.tvName.setText(httpTTS.name) + binding.tvUrl.setText(httpTTS.url) + binding.tvContentType.setText(httpTTS.contentType) + binding.tvConcurrentRate.setText(httpTTS.concurrentRate) + binding.tvLoginUrl.setText(httpTTS.loginUrl) + binding.tvLoginUi.setText(httpTTS.loginUi) + binding.tvLoginCheckJs.setText(httpTTS.loginCheckJs) + binding.tvHeaders.setText(httpTTS.header) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_save -> viewModel.save(dataFromView()) { + toastOnUi("保存成功") + } + R.id.menu_login -> dataFromView().let { httpTts -> + if (httpTts.loginUrl.isNullOrBlank()) { + toastOnUi("登录url不能为空") + } else { + viewModel.save(httpTts) { + startActivity { + putExtra("type", "httpTts") + putExtra("key", httpTts.id.toString()) + } + } + } + } + R.id.menu_show_login_header -> alert { + setTitle(R.string.login_header) + dataFromView().getLoginHeader()?.let { loginHeader -> + setMessage(loginHeader) + } + } + R.id.menu_del_login_header -> dataFromView().removeLoginHeader() + R.id.menu_copy_source -> dataFromView().let { + context?.sendToClip(GSON.toJson(it)) + } + R.id.menu_paste_source -> viewModel.importFromClip { + initView(it) + } + R.id.menu_log -> showDialogFragment() + R.id.menu_help -> showHelp("httpTTSHelp") + } + return true + } + + private fun dataFromView(): HttpTTS { + return HttpTTS( + id = viewModel.id ?: System.currentTimeMillis(), + name = binding.tvName.text.toString(), + url = binding.tvUrl.text.toString(), + contentType = binding.tvContentType.text?.toString(), + concurrentRate = binding.tvConcurrentRate.text?.toString(), + loginUrl = binding.tvLoginUrl.text?.toString(), + loginUi = binding.tvLoginUi.text?.toString(), + loginCheckJs = binding.tvLoginCheckJs.text?.toString(), + header = binding.tvHeaders.text?.toString() + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditViewModel.kt new file mode 100755 index 000000000..17222d770 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/HttpTtsEditViewModel.kt @@ -0,0 +1,76 @@ +package io.legado.app.ui.book.read.config + +import android.app.Application +import android.os.Bundle +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.ReadAloud +import io.legado.app.utils.getClipText +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.toastOnUi + +class HttpTtsEditViewModel(app: Application) : BaseViewModel(app) { + + var id: Long? = null + + fun initData(arguments: Bundle?, success: (httpTTS: HttpTTS) -> Unit) { + execute { + if (id == null) { + val argumentId = arguments?.getLong("id") + if (argumentId != null && argumentId != 0L) { + id = argumentId + return@execute appDb.httpTTSDao.get(argumentId) + } + } + return@execute null + }.onSuccess { + it?.let { + success.invoke(it) + } + } + } + + fun save(httpTTS: HttpTTS, success: (() -> Unit)? = null) { + id = httpTTS.id + execute { + appDb.httpTTSDao.insert(httpTTS) + if (ReadAloud.ttsEngine == httpTTS.id.toString()) ReadAloud.upReadAloudClass() + }.onSuccess { + success?.invoke() + } + } + + fun importFromClip(onSuccess: (httpTTS: HttpTTS) -> Unit) { + val text = context.getClipText() + if (text.isNullOrBlank()) { + context.toastOnUi("剪贴板为空") + } else { + importSource(text, onSuccess) + } + } + + fun importSource(text: String, onSuccess: (httpTTS: HttpTTS) -> Unit) { + val text1 = text.trim() + execute { + when { + text1.isJsonObject() -> { + HttpTTS.fromJson(text1).getOrThrow() + } + text1.isJsonArray() -> { + HttpTTS.fromJsonArray(text1).getOrThrow().first() + } + else -> { + throw NoStackTraceException("格式不对") + } + } + }.onSuccess { + onSuccess.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt new file mode 100755 index 000000000..e1529a17a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt @@ -0,0 +1,211 @@ +package io.legado.app.ui.book.read.config + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.content.SharedPreferences +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewConfiguration +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.base.BasePrefDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.widget.number.NumberPickerDialog +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.postEvent +import io.legado.app.utils.removePref +import io.legado.app.utils.setEdgeEffectColor + +class MoreConfigDialog : BasePrefDialogFragment() { + private val readPreferTag = "readPreferenceFragment" + + override fun onStart() { + super.onStart() + dialog?.window?.run { + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setBackgroundDrawableResource(R.color.background) + decorView.setPadding(0, 0, 0, 0) + val attr = attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + attributes = attr + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 360.dpToPx()) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + (activity as ReadBookActivity).bottomDialog++ + val view = LinearLayout(context) + view.setBackgroundColor(requireContext().bottomBackground) + view.id = R.id.tag1 + container?.addView(view) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + var preferenceFragment = childFragmentManager.findFragmentByTag(readPreferTag) + if (preferenceFragment == null) preferenceFragment = ReadPreferenceFragment() + childFragmentManager.beginTransaction() + .replace(view.id, preferenceFragment, readPreferTag) + .commit() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + + class ReadPreferenceFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val slopSquare by lazy { ViewConfiguration.get(requireContext()).scaledTouchSlop } + + @SuppressLint("RestrictedApi") + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_read) + upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString()) + if (!CanvasRecorderFactory.isSupport) { + removePref(PreferKey.optimizeRender) + preferenceScreen.removePreferenceRecursively(PreferKey.optimizeRender) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listView.setEdgeEffectColor(primaryColor) + } + + override fun onResume() { + super.onResume() + preferenceManager + .sharedPreferences + ?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + preferenceManager + .sharedPreferences + ?.unregisterOnSharedPreferenceChangeListener(this) + super.onPause() + } + + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String? + ) { + when (key) { + PreferKey.readBodyToLh -> activity?.recreate() + PreferKey.hideStatusBar -> { + ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar) + postEvent(EventBus.UP_CONFIG, arrayListOf(0, 2)) + } + + PreferKey.hideNavigationBar -> { + ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar) + postEvent(EventBus.UP_CONFIG, arrayListOf(0, 2)) + } + + PreferKey.keepLight -> postEvent(key, true) + PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key)) + PreferKey.screenOrientation -> { + (activity as? ReadBookActivity)?.setOrientation() + } + + PreferKey.textFullJustify, + PreferKey.textBottomJustify, + PreferKey.useZhLayout -> { + postEvent(EventBus.UP_CONFIG, arrayListOf(5)) + } + + PreferKey.showBrightnessView -> { + postEvent(PreferKey.showBrightnessView, "") + } + + PreferKey.expandTextMenu -> { + (activity as? ReadBookActivity)?.textActionMenu?.upMenu() + } + + PreferKey.doublePageHorizontal -> { + ChapterProvider.upLayout() + ReadBook.loadContent(false) + } + + PreferKey.showReadTitleAddition, + PreferKey.readBarStyleFollowPage -> { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + + PreferKey.progressBarBehavior -> { + postEvent(EventBus.UP_SEEK_BAR, true) + } + + PreferKey.noAnimScrollPage -> { + ReadBook.callBack?.upPageAnim() + } + + PreferKey.optimizeRender -> { + ChapterProvider.upStyle() + ReadBook.callBack?.upPageAnim(true) + ReadBook.loadContent(false) + } + + PreferKey.paddingDisplayCutouts -> { + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + "customPageKey" -> PageKeyDialog(requireContext()).show() + "clickRegionalConfig" -> { + (activity as? ReadBookActivity)?.showClickRegionalConfig() + } + + PreferKey.pageTouchSlop -> { + NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.page_touch_slop_dialog_title)) + .setMaxValue(9999) + .setMinValue(0) + .setValue(AppConfig.pageTouchSlop) + .show { + AppConfig.pageTouchSlop = it + postEvent(EventBus.UP_CONFIG, arrayListOf(4)) + } + } + } + return super.onPreferenceTreeClick(preference) + } + + @Suppress("SameParameterValue") + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.pageTouchSlop -> preference.summary = + getString(R.string.page_touch_slop_summary, value) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt new file mode 100755 index 000000000..6019c782a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt @@ -0,0 +1,124 @@ +package io.legado.app.ui.book.read.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogReadPaddingBinding +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.utils.postEvent +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class PaddingConfigDialog : BaseDialogFragment(R.layout.dialog_read_padding) { + + private val binding by viewBinding(DialogReadPaddingBinding::bind) + + override fun onStart() { + super.onStart() + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val attr = it.attributes + attr.dimAmount = 0.0f + it.attributes = attr + } + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initData() + initView() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + ReadBookConfig.save() + } + + private fun initData() = binding.run { + //正文 + dsbPaddingTop.progress = ReadBookConfig.paddingTop + dsbPaddingBottom.progress = ReadBookConfig.paddingBottom + dsbPaddingLeft.progress = ReadBookConfig.paddingLeft + dsbPaddingRight.progress = ReadBookConfig.paddingRight + //页眉 + dsbHeaderPaddingTop.progress = ReadBookConfig.headerPaddingTop + dsbHeaderPaddingBottom.progress = ReadBookConfig.headerPaddingBottom + dsbHeaderPaddingLeft.progress = ReadBookConfig.headerPaddingLeft + dsbHeaderPaddingRight.progress = ReadBookConfig.headerPaddingRight + //页脚 + dsbFooterPaddingTop.progress = ReadBookConfig.footerPaddingTop + dsbFooterPaddingBottom.progress = ReadBookConfig.footerPaddingBottom + dsbFooterPaddingLeft.progress = ReadBookConfig.footerPaddingLeft + dsbFooterPaddingRight.progress = ReadBookConfig.footerPaddingRight + cbShowTopLine.isChecked = ReadBookConfig.showHeaderLine + cbShowBottomLine.isChecked = ReadBookConfig.showFooterLine + } + + private fun initView() = binding.run { + //正文 + dsbPaddingTop.onChanged = { + ReadBookConfig.paddingTop = it + postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5)) + } + dsbPaddingBottom.onChanged = { + ReadBookConfig.paddingBottom = it + postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5)) + } + dsbPaddingLeft.onChanged = { + ReadBookConfig.paddingLeft = it + postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5)) + } + dsbPaddingRight.onChanged = { + ReadBookConfig.paddingRight = it + postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5)) + } + //页眉 + dsbHeaderPaddingTop.onChanged = { + ReadBookConfig.headerPaddingTop = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + dsbHeaderPaddingBottom.onChanged = { + ReadBookConfig.headerPaddingBottom = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + dsbHeaderPaddingLeft.onChanged = { + ReadBookConfig.headerPaddingLeft = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + dsbHeaderPaddingRight.onChanged = { + ReadBookConfig.headerPaddingRight = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + //页脚 + dsbFooterPaddingTop.onChanged = { + ReadBookConfig.footerPaddingTop = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + dsbFooterPaddingBottom.onChanged = { + ReadBookConfig.footerPaddingBottom = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + dsbFooterPaddingLeft.onChanged = { + ReadBookConfig.footerPaddingLeft = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + dsbFooterPaddingRight.onChanged = { + ReadBookConfig.footerPaddingRight = it + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + cbShowTopLine.onCheckedChangeListener = { _, isChecked -> + ReadBookConfig.showHeaderLine = isChecked + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + cbShowBottomLine.onCheckedChangeListener = { _, isChecked -> + ReadBookConfig.showFooterLine = isChecked + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt new file mode 100755 index 000000000..931d35b84 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt @@ -0,0 +1,72 @@ +package io.legado.app.ui.book.read.config + +import android.app.Dialog +import android.content.Context +import android.view.KeyEvent +import android.view.ViewGroup +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogPageKeyBinding +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.utils.getPrefString +import io.legado.app.utils.hideSoftInput +import io.legado.app.utils.putPrefString +import io.legado.app.utils.setLayout +import splitties.views.onClick + + +class PageKeyDialog(context: Context) : Dialog(context) { + + private val binding = DialogPageKeyBinding.inflate(layoutInflater) + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + init { + setContentView(binding.root) + binding.run { + contentView.setBackgroundColor(context.backgroundColor) + etPrev.setText(context.getPrefString(PreferKey.prevKeys)) + etNext.setText(context.getPrefString(PreferKey.nextKeys)) + tvReset.onClick { + etPrev.setText("") + etNext.setText("") + } + tvOk.setOnClickListener { + context.putPrefString(PreferKey.prevKeys, etPrev.text?.toString()) + context.putPrefString(PreferKey.nextKeys, etNext.text?.toString()) + dismiss() + } + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_DEL) { + if (binding.etPrev.hasFocus()) { + val editableText = binding.etPrev.editableText + if (editableText.isEmpty() or editableText.endsWith(",")) { + editableText.append(keyCode.toString()) + } else { + editableText.append(",").append(keyCode.toString()) + } + return true + } else if (binding.etNext.hasFocus()) { + val editableText = binding.etNext.editableText + if (editableText.isEmpty() or editableText.endsWith(",")) { + editableText.append(keyCode.toString()) + } else { + editableText.append(",").append(keyCode.toString()) + } + return true + } + } + return super.onKeyDown(keyCode, event) + } + + override fun dismiss() { + super.dismiss() + currentFocus?.hideSoftInput() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt new file mode 100755 index 000000000..81c3129db --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt @@ -0,0 +1,151 @@ +package io.legado.app.ui.book.read.config + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.preference.ListPreference +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.base.BasePrefDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.help.IntentHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.prefs.SwitchPreference +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.ReadAloud +import io.legado.app.service.BaseReadAloudService +import io.legado.app.utils.GSON +import io.legado.app.utils.StringUtils +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.postEvent +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment + +class ReadAloudConfigDialog : BasePrefDialogFragment() { + private val readAloudPreferTag = "readAloudPreferTag" + + override fun onStart() { + super.onStart() + dialog?.window?.run { + setBackgroundDrawableResource(R.color.transparent) + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = LinearLayout(requireContext()) + view.setBackgroundColor(requireContext().backgroundColor) + view.id = R.id.tag1 + container?.addView(view) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + var preferenceFragment = childFragmentManager.findFragmentByTag(readAloudPreferTag) + if (preferenceFragment == null) preferenceFragment = ReadAloudPreferenceFragment() + childFragmentManager.beginTransaction() + .replace(view.id, preferenceFragment, readAloudPreferTag) + .commit() + } + + class ReadAloudPreferenceFragment : PreferenceFragment(), + SpeakEngineDialog.CallBack, + SharedPreferences.OnSharedPreferenceChangeListener { + + private val speakEngineSummary: String + get() { + val ttsEngine = ReadAloud.ttsEngine + ?: return getString(R.string.system_tts) + if (StringUtils.isNumeric(ttsEngine)) { + return appDb.httpTTSDao.getName(ttsEngine.toLong()) + ?: getString(R.string.system_tts) + } + return GSON.fromJsonObject>(ttsEngine).getOrNull()?.title + ?: getString(R.string.system_tts) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_aloud) + upSpeakEngineSummary() + findPreference(PreferKey.pauseReadAloudWhilePhoneCalls)?.let { + it.isEnabled = AppConfig.ignoreAudioFocus + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listView.setEdgeEffectColor(primaryColor) + } + + override fun onResume() { + super.onResume() + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + super.onPause() + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + PreferKey.ttsEngine -> showDialogFragment(SpeakEngineDialog()) + "sysTtsConfig" -> IntentHelp.openTTSSetting() + } + return super.onPreferenceTreeClick(preference) + } + + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String? + ) { + when (key) { + PreferKey.readAloudByPage, PreferKey.streamReadAloudAudio -> { + if (BaseReadAloudService.isRun) { + postEvent(EventBus.MEDIA_BUTTON, false) + } + } + + PreferKey.ignoreAudioFocus -> { + findPreference(PreferKey.pauseReadAloudWhilePhoneCalls)?.let { + it.isEnabled = AppConfig.ignoreAudioFocus + } + } + } + } + + private fun upPreferenceSummary(preference: Preference?, value: String) { + when (preference) { + is ListPreference -> { + val index = preference.findIndexOfValue(value) + preference.summary = if (index >= 0) preference.entries[index] else null + } + + else -> { + preference?.summary = value + } + } + } + + override fun upSpeakEngineSummary() { + upPreferenceSummary( + findPreference(PreferKey.ttsEngine), + speakEngineSummary + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt new file mode 100755 index 000000000..56928ef77 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt @@ -0,0 +1,232 @@ +package io.legado.app.ui.book.read.config + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.SeekBar +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogReadAloudBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding + + +class ReadAloudDialog : BaseDialogFragment(R.layout.dialog_read_aloud) { + private val callBack: CallBack? get() = activity as? CallBack + private val binding by viewBinding(DialogReadAloudBinding::bind) + + override fun onStart() { + super.onStart() + dialog?.window?.run { + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setBackgroundDrawableResource(R.color.background) + decorView.setPadding(0, 0, 0, 0) + val attr = attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + attributes = attr + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + (activity as ReadBookActivity).bottomDialog-- + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val bottomDialog = (activity as ReadBookActivity).bottomDialog++ + if (bottomDialog > 0) { + dismiss() + return + } + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + binding.run { + rootView.setBackgroundColor(bg) + tvPre.setTextColor(textColor) + tvNext.setTextColor(textColor) + ivPlayPrev.setColorFilter(textColor) + ivPlayPause.setColorFilter(textColor) + ivPlayNext.setColorFilter(textColor) + ivStop.setColorFilter(textColor) + ivTimer.setColorFilter(textColor) + tvTimer.setTextColor(textColor) + ivTtsSpeechReduce.setColorFilter(textColor) + tvTtsSpeed.setTextColor(textColor) + tvTtsSpeedValue.setTextColor(textColor) + ivTtsSpeechAdd.setColorFilter(textColor) + ivCatalog.setColorFilter(textColor) + tvCatalog.setTextColor(textColor) + ivMainMenu.setColorFilter(textColor) + tvMainMenu.setTextColor(textColor) + ivToBackstage.setColorFilter(textColor) + tvToBackstage.setTextColor(textColor) + ivSetting.setColorFilter(textColor) + tvSetting.setTextColor(textColor) + cbTtsFollowSys.setTextColor(textColor) + } + initData() + initEvent() + } + + private fun initData() = binding.run { + upPlayState() + upTimerText(BaseReadAloudService.timeMinute) + cbTtsFollowSys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) + upTtsSpeechRateEnabled(!cbTtsFollowSys.isChecked) + upSeekTimer() + } + + private fun initEvent() = binding.run { + llMainMenu.setOnClickListener { + callBack?.showMenuBar() + dismissAllowingStateLoss() + } + llSetting.setOnClickListener { + ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") + } + tvPre.setOnClickListener { ReadBook.moveToPrevChapter(upContent = true, toLast = false) } + tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) } + ivStop.setOnClickListener { + ReadAloud.stop(requireContext()) + dismissAllowingStateLoss() + } + ivPlayPause.setOnClickListener { callBack?.onClickReadAloud() } + ivPlayPrev.setOnClickListener { ReadAloud.prevParagraph(requireContext()) } + ivPlayNext.setOnClickListener { ReadAloud.nextParagraph(requireContext()) } + llCatalog.setOnClickListener { callBack?.openChapterList() } + llToBackstage.setOnClickListener { callBack?.finish() } + cbTtsFollowSys.setOnCheckedChangeListener { _, isChecked -> + AppConfig.ttsFlowSys = isChecked + upTtsSpeechRateEnabled(!isChecked) + upTtsSpeechRate() + } + ivTtsSpeechReduce.setOnClickListener { + seekTtsSpeechRate.progress = AppConfig.ttsSpeechRate - 1 + AppConfig.ttsSpeechRate -= 1 + upTtsSpeechRate() + } + ivTtsSpeechAdd.setOnClickListener { + seekTtsSpeechRate.progress = AppConfig.ttsSpeechRate + 1 + AppConfig.ttsSpeechRate += 1 + upTtsSpeechRate() + } + ivTimer.setOnClickListener { + AppConfig.ttsTimer = seekTimer.progress + toastOnUi("保存设定时间成功!") + } + tvTimer.setOnClickListener { + val times = intArrayOf(0, 5, 10, 15, 30, 60, 90, 180) + val timeKeys = times.map { "$it 分钟" } + context?.selector("设定时间", timeKeys) { _, index -> + ReadAloud.setTimer(requireContext(), times[index]) + } + } + //设置保存的默认值 + seekTtsSpeechRate.progress = AppConfig.ttsSpeechRate + seekTtsSpeechRate.setOnSeekBarChangeListener(object : SeekBarChangeListener { + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + super.onProgressChanged(seekBar, progress, fromUser) + upTtsSpeechRateText(progress) + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + AppConfig.ttsSpeechRate = seekBar.progress + upTtsSpeechRate() + } + }) + seekTimer.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + upTimerText(progress) + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + ReadAloud.setTimer(requireContext(), seekTimer.progress) + } + }) + } + + private fun upTtsSpeechRateEnabled(enabled: Boolean) { + binding.run { + upTtsSpeechRateText(AppConfig.ttsSpeechRate) + tvTtsSpeedValue.visible(enabled) + seekTtsSpeechRate.isEnabled = enabled + ivTtsSpeechReduce.isEnabled = enabled + ivTtsSpeechAdd.isEnabled = enabled + } + } + + private fun upPlayState() { + if (!BaseReadAloudService.pause) { + binding.ivPlayPause.setImageResource(R.drawable.ic_pause_24dp) + binding.ivPlayPause.contentDescription = getString(R.string.pause) + } else { + binding.ivPlayPause.setImageResource(R.drawable.ic_play_24dp) + binding.ivPlayPause.contentDescription = getString(R.string.audio_play) + } + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + binding.ivPlayPause.setColorFilter(textColor) + } + + private fun upSeekTimer() { + binding.seekTimer.post { + if (BaseReadAloudService.timeMinute > 0) { + binding.seekTimer.progress = BaseReadAloudService.timeMinute + } else { + binding.seekTimer.progress = AppConfig.ttsTimer + } + } + } + + private fun upTimerText(timeMinute: Int) { + if (timeMinute < 0) { + binding.tvTimer.text = requireContext().getString(R.string.timer_m, 0) + } else { + binding.tvTimer.text = requireContext().getString(R.string.timer_m, timeMinute) + } + } + + @SuppressLint("SetTextI18n") + private fun upTtsSpeechRateText(value: Int) { + binding.tvTtsSpeedValue.text = ((value + 5) / 10f).toString() + } + + private fun upTtsSpeechRate() { + ReadAloud.upTtsSpeechRate(requireContext()) + if (!BaseReadAloudService.pause) { + ReadAloud.pause(requireContext()) + ReadAloud.resume(requireContext()) + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.ALOUD_STATE) { upPlayState() } + observeEvent(EventBus.READ_ALOUD_DS) { binding.seekTimer.progress = it } + } + + interface CallBack { + fun showMenuBar() + fun openChapterList() + fun onClickReadAloud() + fun finish() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt new file mode 100755 index 000000000..ed3736dc6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt @@ -0,0 +1,254 @@ +package io.legado.app.ui.book.read.config + +import android.content.DialogInterface +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.core.view.get +import com.github.liuyueyi.quick.transfer.constants.TransType +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogReadBookStyleBinding +import io.legado.app.databinding.ItemReadStyleBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.font.FontSelectDialog +import io.legado.app.utils.ChineseUtils +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getIndexById +import io.legado.app.utils.postEvent +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.views.onLongClick + +class ReadStyleDialog : BaseDialogFragment(R.layout.dialog_read_book_style), + FontSelectDialog.CallBack { + + private val binding by viewBinding(DialogReadBookStyleBinding::bind) + private val callBack get() = activity as? ReadBookActivity + private lateinit var styleAdapter: StyleAdapter + + override fun onStart() { + super.onStart() + dialog?.window?.run { + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setBackgroundDrawableResource(R.color.background) + decorView.setPadding(0, 0, 0, 0) + val attr = attributes + attr.dimAmount = 0.0f + attr.gravity = Gravity.BOTTOM + attributes = attr + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + (activity as ReadBookActivity).bottomDialog++ + initView() + initData() + initViewEvent() + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + ReadBookConfig.save() + (activity as ReadBookActivity).bottomDialog-- + } + + private fun initView() = binding.run { + val bg = requireContext().bottomBackground + val isLight = ColorUtils.isColorLight(bg) + val textColor = requireContext().getPrimaryTextColor(isLight) + rootView.setBackgroundColor(bg) + tvPageAnim.setTextColor(textColor) + tvBgTs.setTextColor(textColor) + tvShareLayout.setTextColor(textColor) + dsbTextSize.valueFormat = { + (it + 5).toString() + } + dsbTextLetterSpacing.valueFormat = { + ((it - 50) / 100f).toString() + } + dsbLineSize.valueFormat = { ((it - 10) / 10f).toString() } + dsbParagraphSpacing.valueFormat = { (it / 10f).toString() } + styleAdapter = StyleAdapter() + rvStyle.adapter = styleAdapter + styleAdapter.addFooterView { + ItemReadStyleBinding.inflate(layoutInflater, it, false).apply { + ivStyle.setPadding(6.dpToPx(), 6.dpToPx(), 6.dpToPx(), 6.dpToPx()) + ivStyle.setText(null) + ivStyle.setColorFilter(textColor) + ivStyle.borderColor = textColor + ivStyle.setImageResource(R.drawable.ic_add) + root.setOnClickListener { + ReadBookConfig.configList.add(ReadBookConfig.Config()) + showBgTextConfig(ReadBookConfig.configList.lastIndex) + } + } + } + } + + private fun initData() { + binding.cbShareLayout.isChecked = ReadBookConfig.shareLayout + upView() + styleAdapter.setItems(ReadBookConfig.configList) + } + + private fun initViewEvent() = binding.run { + chineseConverter.onChanged { + ChineseUtils.unLoad(*TransType.entries.toTypedArray()) + postEvent(EventBus.UP_CONFIG, arrayListOf(5)) + } + textFontWeightConverter.onChanged { + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 9, 6)) + } + tvTextFont.setOnClickListener { + showDialogFragment() + } + tvTextIndent.setOnClickListener { + context?.selector( + title = getString(R.string.text_indent), + items = resources.getStringArray(R.array.indent).toList() + ) { _, index -> + ReadBookConfig.paragraphIndent = " ".repeat(index) + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + } + tvPadding.setOnClickListener { + dismissAllowingStateLoss() + callBack?.showPaddingConfig() + } + tvTip.setOnClickListener { + TipConfigDialog().show(childFragmentManager, "tipConfigDialog") + } + rgPageAnim.setOnCheckedChangeListener { _, checkedId -> + ReadBook.book?.setPageAnim(-1) + ReadBookConfig.pageAnim = binding.rgPageAnim.getIndexById(checkedId) + callBack?.upPageAnim() + ReadBook.loadContent(false) + } + cbShareLayout.onCheckedChangeListener = { _, isChecked -> + ReadBookConfig.shareLayout = isChecked + upView() + postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) + } + dsbTextSize.onChanged = { + ReadBookConfig.textSize = it + 5 + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + dsbTextLetterSpacing.onChanged = { + ReadBookConfig.letterSpacing = (it - 50) / 100f + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + dsbLineSize.onChanged = { + ReadBookConfig.lineSpacingExtra = it + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + dsbParagraphSpacing.onChanged = { + ReadBookConfig.paragraphSpacing = it + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + } + + private fun changeBgTextConfig(index: Int) { + val oldIndex = ReadBookConfig.styleSelect + if (index != oldIndex) { + ReadBookConfig.styleSelect = index + upView() + styleAdapter.notifyItemChanged(oldIndex) + styleAdapter.notifyItemChanged(index) + postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + } + + private fun showBgTextConfig(index: Int): Boolean { + dismissAllowingStateLoss() + changeBgTextConfig(index) + callBack?.showBgTextConfig() + return true + } + + private fun upView() = binding.run { + textFontWeightConverter.upUi(ReadBookConfig.textBold) + ReadBook.pageAnim().let { + if (it >= 0 && it < rgPageAnim.childCount) { + rgPageAnim.check(rgPageAnim[it].id) + } + } + ReadBookConfig.let { + dsbTextSize.progress = it.textSize - 5 + dsbTextLetterSpacing.progress = (it.letterSpacing * 100).toInt() + 50 + dsbLineSize.progress = it.lineSpacingExtra + dsbParagraphSpacing.progress = it.paragraphSpacing + } + } + + override val curFontPath: String + get() = ReadBookConfig.textFont + + override fun selectFont(path: String) { + if (path != ReadBookConfig.textFont || path.isEmpty()) { + ReadBookConfig.textFont = path + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 5)) + } + } + + inner class StyleAdapter : + RecyclerAdapter(requireContext()) { + + override fun getViewBinding(parent: ViewGroup): ItemReadStyleBinding { + return ItemReadStyleBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemReadStyleBinding, + item: ReadBookConfig.Config, + payloads: MutableList + ) { + binding.apply { + ivStyle.setText(item.name.ifBlank { "文字" }) + ivStyle.setTextColor(item.curTextColor()) + ivStyle.setImageDrawable(item.curBgDrawable(100, 150)) + if (ReadBookConfig.styleSelect == holder.layoutPosition) { + ivStyle.borderColor = accentColor + ivStyle.setTextBold(true) + } else { + ivStyle.borderColor = item.curTextColor() + ivStyle.setTextBold(false) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemReadStyleBinding) { + binding.apply { + ivStyle.setOnClickListener { + if (ivStyle.isInView) { + changeBgTextConfig(holder.layoutPosition) + } + } + ivStyle.onLongClick(ivStyle.isInView) { + if (ivStyle.isInView) { + showBgTextConfig(holder.layoutPosition) + } + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt new file mode 100755 index 000000000..0b14dd45d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt @@ -0,0 +1,291 @@ +package io.legado.app.ui.book.read.config + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.RadioButton +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemHttpTtsBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.ui.association.ImportHttpTtsDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.utils.ACache +import io.legado.app.utils.GSON +import io.legado.app.utils.applyTint +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.gone +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * tts引擎管理 + */ +class SpeakEngineDialog() : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel: SpeakEngineViewModel by viewModels() + private val ttsUrlKey = "ttsUrlKey" + private val adapter by lazy { Adapter(requireContext()) } + private var ttsEngine: String? = ReadAloud.ttsEngine + private val sysTtsViews = arrayListOf() + private val callBack: CallBack? get() = parentFragment as? CallBack + private val importDocResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportHttpTtsDialog(uri.toString())) + } + } + private val exportDirResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + requireContext().sendToClip(uri.toString()) + } + } + } + } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + initMenu() + initData() + } + + private fun initView() = binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.setTitle(R.string.speak_engine) + recyclerView.setEdgeEffectColor(primaryColor) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter + adapter.addHeaderView { + ItemHttpTtsBinding.inflate(layoutInflater, recyclerView, false).apply { + sysTtsViews.add(cbName) + ivEdit.gone() + ivMenuDelete.gone() + labelSys.visible() + cbName.text = "系统默认" + cbName.tag = "" + cbName.isChecked = ttsEngine == null || ttsEngine!!.isJsonObject() + && GSON.fromJsonObject>(ttsEngine) + .getOrNull()?.value.isNullOrEmpty() + cbName.setOnClickListener { + upTts(GSON.toJson(SelectItem("系统默认", ""))) + } + } + } + viewModel.sysEngines.forEach { engine -> + adapter.addHeaderView { + ItemHttpTtsBinding.inflate(layoutInflater, recyclerView, false).apply { + sysTtsViews.add(cbName) + ivEdit.gone() + ivMenuDelete.gone() + labelSys.visible() + cbName.text = engine.label + cbName.tag = engine.name + cbName.isChecked = GSON.fromJsonObject>(ttsEngine) + .getOrNull()?.value == cbName.tag + cbName.setOnClickListener { + upTts(GSON.toJson(SelectItem(engine.label, engine.name))) + } + } + } + } + tvFooterLeft.setText(R.string.book) + tvFooterLeft.visible() + tvFooterLeft.setOnClickListener { + ReadBook.book?.setTtsEngine(ttsEngine) + callBack?.upSpeakEngineSummary() + ReadAloud.upReadAloudClass() + dismissAllowingStateLoss() + } + tvOk.setText(R.string.general) + tvOk.visible() + tvOk.setOnClickListener { + ReadBook.book?.setTtsEngine(null) + AppConfig.ttsEngine = ttsEngine + callBack?.upSpeakEngineSummary() + ReadAloud.upReadAloudClass() + dismissAllowingStateLoss() + } + tvCancel.visible() + tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + } + + private fun initMenu() = binding.run { + toolBar.inflateMenu(R.menu.speak_engine) + toolBar.menu.applyTint(requireContext()) + toolBar.setOnMenuItemClickListener(this@SpeakEngineDialog) + } + + private fun initData() { + lifecycleScope.launch { + appDb.httpTTSDao.flowAll().catch { + AppLog.put("朗读引擎界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> showDialogFragment() + R.id.menu_default -> viewModel.importDefault() + R.id.menu_import_local -> importDocResult.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_onLine -> importAlert() + R.id.menu_export -> exportDirResult.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "httpTts.json", + GSON.toJson(adapter.getItems()).toByteArray(), + "application/json" + ) + } + } + return true + } + + private fun importAlert() { + val aCache = ACache.get(cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(ttsUrlKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(ttsUrlKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { url -> + if (url.isAbsUrl() && !cacheUrls.contains(url)) { + cacheUrls.add(0, url) + aCache.put(ttsUrlKey, cacheUrls.joinToString(",")) + } + showDialogFragment(ImportHttpTtsDialog(url)) + } + } + } + } + + private fun upTts(tts: String) { + ttsEngine = tts + sysTtsViews.forEach { + it.isChecked = GSON.fromJsonObject>(ttsEngine) + .getOrNull()?.value == it.tag + } + adapter.notifyItemRangeChanged(adapter.getHeaderCount(), adapter.itemCount) + } + + inner class Adapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemHttpTtsBinding { + return ItemHttpTtsBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemHttpTtsBinding, + item: HttpTTS, + payloads: MutableList + ) { + binding.apply { + cbName.text = item.name + cbName.isChecked = item.id.toString() == ttsEngine + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemHttpTtsBinding) { + binding.run { + cbName.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { httpTTS -> + val id = httpTTS.id.toString() + upTts(id) + if (!httpTTS.loginUrl.isNullOrBlank() + && httpTTS.getLoginInfo().isNullOrBlank() + ) { + startActivity { + putExtra("type", "httpTts") + putExtra("key", id) + } + } + } + } + ivEdit.setOnClickListener { + val id = getItemByLayoutPosition(holder.layoutPosition)!!.id + showDialogFragment(HttpTtsEditDialog(id)) + } + ivMenuDelete.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { httpTTS -> + appDb.httpTTSDao.delete(httpTTS) + } + } + } + } + + } + + interface CallBack { + fun upSpeakEngineSummary() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt new file mode 100755 index 000000000..ae9ab7b90 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt @@ -0,0 +1,23 @@ +package io.legado.app.ui.book.read.config + +import android.app.Application +import android.speech.tts.TextToSpeech +import io.legado.app.base.BaseViewModel +import io.legado.app.help.DefaultData + +class SpeakEngineViewModel(application: Application) : BaseViewModel(application) { + + val sysEngines: List by lazy { + val tts = TextToSpeech(context, null) + val engines = tts.engines + tts.shutdown() + engines + } + + fun importDefault() { + execute { + DefaultData.importDefaultHttpTTS() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt new file mode 100755 index 000000000..de0d4ca56 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TextFontWeightConverter.kt @@ -0,0 +1,56 @@ +package io.legado.app.ui.book.read.config + +import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.AttributeSet +import io.legado.app.R +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.widget.text.StrokeTextView + + +class TextFontWeightConverter(context: Context, attrs: AttributeSet?) : + StrokeTextView(context, attrs) { + + private val spannableString = SpannableString(context.getString(R.string.font_weight_text)) + private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor) + private var onChanged: (() -> Unit)? = null + + init { + text = spannableString + if (!isInEditMode) { + upUi(ReadBookConfig.textBold) + } + setOnClickListener { + selectType() + } + } + + @Suppress("MemberVisibilityCanBePrivate") + fun upUi(type: Int) { + spannableString.removeSpan(enabledSpan) + when (type) { + 0 -> spannableString.setSpan(enabledSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 1 -> spannableString.setSpan(enabledSpan, 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 2 -> spannableString.setSpan(enabledSpan, 4, 5, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + text = spannableString + } + + private fun selectType() { + context.alert(titleResource = R.string.text_font_weight_converter) { + items(context.resources.getStringArray(R.array.text_font_weight).toList()) { _, i -> + ReadBookConfig.textBold = i + upUi(i) + onChanged?.invoke() + } + } + } + + fun onChanged(unit: () -> Unit) { + onChanged = unit + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt new file mode 100755 index 000000000..398428747 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt @@ -0,0 +1,252 @@ +package io.legado.app.ui.book.read.config + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.indices +import com.jaredrummler.android.colorpicker.ColorPickerDialog +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.databinding.DialogTipConfigBinding +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ReadTipConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.utils.checkByIndex +import io.legado.app.utils.getIndexById +import io.legado.app.utils.hexString +import io.legado.app.utils.observeEvent +import io.legado.app.utils.postEvent +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + + +class TipConfigDialog : BaseDialogFragment(R.layout.dialog_tip_config) { + + companion object { + const val TIP_COLOR = 7897 + const val TIP_DIVIDER_COLOR = 7898 + } + + private val binding by viewBinding(DialogTipConfigBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + initEvent() + observeEvent(EventBus.TIP_COLOR) { + upTvTipColor() + upTvTipDividerColor() + } + } + + private fun initView() { + if (ReadBookConfig.titleMode !in binding.rgTitleMode.indices) { + ReadBookConfig.titleMode = 0 + } + binding.rgTitleMode.checkByIndex(ReadBookConfig.titleMode) + binding.dsbTitleSize.progress = ReadBookConfig.titleSize + binding.dsbTitleTop.progress = ReadBookConfig.titleTopSpacing + binding.dsbTitleBottom.progress = ReadBookConfig.titleBottomSpacing + + binding.tvHeaderShow.text = + ReadTipConfig.getHeaderModes(requireContext())[ReadTipConfig.headerMode] + binding.tvFooterShow.text = + ReadTipConfig.getFooterModes(requireContext())[ReadTipConfig.footerMode] + + ReadTipConfig.run { + tipNames.let { tipNames -> + binding.tvHeaderLeft.text = + tipNames.getOrElse(tipValues.indexOf(tipHeaderLeft)) { tipNames[none] } + binding.tvHeaderMiddle.text = + tipNames.getOrElse(tipValues.indexOf(tipHeaderMiddle)) { tipNames[none] } + binding.tvHeaderRight.text = + tipNames.getOrElse(tipValues.indexOf(tipHeaderRight)) { tipNames[none] } + binding.tvFooterLeft.text = + tipNames.getOrElse(tipValues.indexOf(tipFooterLeft)) { tipNames[none] } + binding.tvFooterMiddle.text = + tipNames.getOrElse(tipValues.indexOf(tipFooterMiddle)) { tipNames[none] } + binding.tvFooterRight.text = + tipNames.getOrElse(tipValues.indexOf(tipFooterRight)) { tipNames[none] } + } + } + upTvTipColor() + upTvTipDividerColor() + } + + private fun upTvTipColor() { + val tipColorNames = ReadTipConfig.tipColorNames + val tipColor = ReadTipConfig.tipColor + binding.tvTipColor.text = if (tipColor == 0) { + tipColorNames.first() + } else { + "#${tipColor.hexString}" + } + } + + private fun upTvTipDividerColor() { + val tipDividerColorNames = ReadTipConfig.tipDividerColorNames + val tipDividerColor = ReadTipConfig.tipDividerColor + binding.tvTipDividerColor.text = when (tipDividerColor) { + -1, 0 -> tipDividerColorNames[tipDividerColor + 1] + else -> "#${tipDividerColor.hexString}" + } + } + + private fun initEvent() = binding.run { + rgTitleMode.setOnCheckedChangeListener { _, checkedId -> + ReadBookConfig.titleMode = rgTitleMode.getIndexById(checkedId) + postEvent(EventBus.UP_CONFIG, arrayListOf(5)) + } + dsbTitleSize.onChanged = { + ReadBookConfig.titleSize = it + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + dsbTitleTop.onChanged = { + ReadBookConfig.titleTopSpacing = it + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + dsbTitleBottom.onChanged = { + ReadBookConfig.titleBottomSpacing = it + postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5)) + } + llHeaderShow.setOnClickListener { + val headerModes = ReadTipConfig.getHeaderModes(requireContext()) + context?.selector(items = headerModes.values.toList()) { _, i -> + ReadTipConfig.headerMode = headerModes.keys.toList()[i] + tvHeaderShow.text = headerModes[ReadTipConfig.headerMode] + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + } + llFooterShow.setOnClickListener { + val footerModes = ReadTipConfig.getFooterModes(requireContext()) + context?.selector(items = footerModes.values.toList()) { _, i -> + ReadTipConfig.footerMode = footerModes.keys.toList()[i] + tvFooterShow.text = footerModes[ReadTipConfig.footerMode] + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + } + llHeaderLeft.setOnClickListener { + context?.selector(items = ReadTipConfig.tipNames) { _, i -> + val tipValue = ReadTipConfig.tipValues[i] + clearRepeat(tipValue) + ReadTipConfig.tipHeaderLeft = tipValue + tvHeaderLeft.text = ReadTipConfig.tipNames[i] + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6)) + } + } + llHeaderMiddle.setOnClickListener { + context?.selector(items = ReadTipConfig.tipNames) { _, i -> + val tipValue = ReadTipConfig.tipValues[i] + clearRepeat(tipValue) + ReadTipConfig.tipHeaderMiddle = tipValue + tvHeaderMiddle.text = ReadTipConfig.tipNames[i] + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6)) + } + } + llHeaderRight.setOnClickListener { + context?.selector(items = ReadTipConfig.tipNames) { _, i -> + val tipValue = ReadTipConfig.tipValues[i] + clearRepeat(tipValue) + ReadTipConfig.tipHeaderRight = tipValue + tvHeaderRight.text = ReadTipConfig.tipNames[i] + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6)) + } + } + llFooterLeft.setOnClickListener { + context?.selector(items = ReadTipConfig.tipNames) { _, i -> + val tipValue = ReadTipConfig.tipValues[i] + clearRepeat(tipValue) + ReadTipConfig.tipFooterLeft = tipValue + tvFooterLeft.text = ReadTipConfig.tipNames[i] + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6)) + } + } + llFooterMiddle.setOnClickListener { + context?.selector(items = ReadTipConfig.tipNames) { _, i -> + val tipValue = ReadTipConfig.tipValues[i] + clearRepeat(tipValue) + ReadTipConfig.tipFooterMiddle = tipValue + tvFooterMiddle.text = ReadTipConfig.tipNames[i] + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6)) + } + } + llFooterRight.setOnClickListener { + context?.selector(items = ReadTipConfig.tipNames) { _, i -> + val tipValue = ReadTipConfig.tipValues[i] + clearRepeat(tipValue) + ReadTipConfig.tipFooterRight = tipValue + tvFooterRight.text = ReadTipConfig.tipNames[i] + postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6)) + } + } + llTipColor.setOnClickListener { + context?.selector(items = ReadTipConfig.tipColorNames) { _, i -> + when (i) { + 0 -> { + ReadTipConfig.tipColor = 0 + upTvTipColor() + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + + 1 -> ColorPickerDialog.newBuilder() + .setShowAlphaSlider(false) + .setDialogType(ColorPickerDialog.TYPE_CUSTOM) + .setDialogId(TIP_COLOR) + .show(requireActivity()) + } + } + } + llTipDividerColor.setOnClickListener { + context?.selector(items = ReadTipConfig.tipDividerColorNames) { _, i -> + when (i) { + 0, 1 -> { + ReadTipConfig.tipDividerColor = i - 1 + upTvTipDividerColor() + postEvent(EventBus.UP_CONFIG, arrayListOf(2)) + } + + 2 -> ColorPickerDialog.newBuilder() + .setShowAlphaSlider(false) + .setDialogType(ColorPickerDialog.TYPE_CUSTOM) + .setDialogId(TIP_DIVIDER_COLOR) + .show(requireActivity()) + } + } + } + } + + private fun clearRepeat(repeat: Int) = ReadTipConfig.apply { + if (repeat != none) { + if (tipHeaderLeft == repeat) { + tipHeaderLeft = none + binding.tvHeaderLeft.text = tipNames[none] + } + if (tipHeaderMiddle == repeat) { + tipHeaderMiddle = none + binding.tvHeaderMiddle.text = tipNames[none] + } + if (tipHeaderRight == repeat) { + tipHeaderRight = none + binding.tvHeaderRight.text = tipNames[none] + } + if (tipFooterLeft == repeat) { + tipFooterLeft = none + binding.tvFooterLeft.text = tipNames[none] + } + if (tipFooterMiddle == repeat) { + tipFooterMiddle = none + binding.tvFooterMiddle.text = tipNames[none] + } + if (tipFooterRight == repeat) { + tipFooterRight = none + binding.tvFooterRight.text = tipNames[none] + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt b/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt new file mode 100755 index 000000000..3775383b3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt @@ -0,0 +1,167 @@ +package io.legado.app.ui.book.read.page + +import android.graphics.Canvas +import android.graphics.Paint +import android.os.SystemClock +import androidx.core.graphics.withClip +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.canvasrecorder.recordIfNeeded + +/** + * 自动翻页 + */ +class AutoPager(private val readView: ReadView) : Runnable { + private var progress = 0 + var isRunning = false + private set + private var isPausing = false + private var isEInkMode = false + private var scrollOffsetRemain = 0.0 + private var scrollOffset = 0 + private var lastTimeMillis = 0L + private var canvasRecorder = CanvasRecorderFactory.create() + private val paint by lazy { Paint() } + + + fun start() { + isRunning = true + isEInkMode = AppConfig.isEInkMode + readView.curPage.upSelectAble(false) + if (isEInkMode) { + readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L) + } else { + paint.color = ThemeStore.accentColor + lastTimeMillis = SystemClock.uptimeMillis() + readView.invalidate() + } + } + + fun stop() { + if (!isRunning) { + return + } + isRunning = false + isPausing = false + isEInkMode = false + readView.removeCallbacks(this) + readView.curPage.upSelectAble(AppConfig.textSelectAble) + readView.invalidate() + reset() + canvasRecorder.recycle() + } + + fun pause() { + if (!isRunning) { + return + } + isPausing = true + readView.removeCallbacks(this) + } + + fun resume() { + if (!isRunning) { + return + } + isPausing = false + if (isEInkMode) { + readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L) + } else { + lastTimeMillis = SystemClock.uptimeMillis() + readView.invalidate() + } + } + + fun reset() { + if (isEInkMode) { + readView.removeCallbacks(this) + readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L) + } else { + progress = 0 + scrollOffsetRemain = 0.0 + scrollOffset = 0 + canvasRecorder.invalidate() + } + } + + fun upRecorder() { + canvasRecorder.recycle() + canvasRecorder = CanvasRecorderFactory.create() + } + + fun onDraw(canvas: Canvas) { + if (!isRunning || isEInkMode) { + return + } + + if (readView.isScroll) { + if (!isPausing) { + readView.curPage.scroll(-scrollOffset) + scrollOffset = 0 + } + } else { + val bottom = progress + val width = readView.width + + canvasRecorder.recordIfNeeded(readView.nextPage) + canvas.withClip(0, 0, width, bottom) { + canvasRecorder.draw(this) + } + + canvas.drawRect( + 0f, + bottom.toFloat() - 1, + width.toFloat(), + bottom.toFloat(), + paint + ) + if (!isPausing) readView.postInvalidate() + } + + } + + fun computeOffset() { + if (!isRunning || isPausing || isEInkMode) { + return + } + + val currentTime = SystemClock.uptimeMillis() + val elapsedTime = currentTime - lastTimeMillis + lastTimeMillis = currentTime + + val readTime = ReadBookConfig.autoReadSpeed * 1000.0 + val height = readView.height + scrollOffsetRemain += height / readTime * elapsedTime + if (scrollOffsetRemain < 1) { + return + } + scrollOffset = scrollOffsetRemain.toInt() + this.scrollOffsetRemain -= scrollOffset + if (!readView.isScroll) { + progress += scrollOffset + if (progress >= height) { + if (!readView.fillPage(PageDirection.NEXT)) { + stop() + } else { + reset() + } + } + } + } + + override fun run() { + if (!isRunning || isPausing) { + return + } + + if (!readView.fillPage(PageDirection.NEXT)) { + stop() + } else { + readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt new file mode 100755 index 000000000..8cb4351f6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -0,0 +1,717 @@ +package io.legado.app.ui.book.read.page + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import io.legado.app.R +import io.legado.app.data.entities.Bookmark +import io.legado.app.help.config.AppConfig +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.delegate.PageDelegate +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.entities.TextPos +import io.legado.app.ui.book.read.page.entities.column.BaseColumn +import io.legado.app.ui.book.read.page.entities.column.ButtonColumn +import io.legado.app.ui.book.read.page.entities.column.ImageColumn +import io.legado.app.ui.book.read.page.entities.column.ReviewColumn +import io.legado.app.ui.book.read.page.entities.column.TextColumn +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.TextPageFactory +import io.legado.app.ui.widget.dialog.PhotoDialog +import io.legado.app.utils.activity +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import java.util.concurrent.Executors +import kotlin.math.max +import kotlin.math.min + +/** + * 阅读内容视图 + */ +class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { + var selectAble = AppConfig.textSelectAble + val selectedPaint by lazy { + Paint().apply { + color = context.getCompatColor(R.color.btn_bg_press_2) + style = Paint.Style.FILL + } + } + private var callBack: CallBack + private val visibleRect = ChapterProvider.visibleRect + val selectStart = TextPos(0, -1, -1) + private val selectEnd = TextPos(0, -1, -1) + var textPage: TextPage = TextPage() + private set + var isMainView = false + var longScreenshot = false + var reverseStartCursor = false + var reverseEndCursor = false + + //滚动参数 + private val pageFactory get() = callBack.pageFactory + private val pageDelegate get() = callBack.pageDelegate + private var pageOffset = 0 + private var autoPager: AutoPager? = null + private var isScroll = false + private val renderRunnable by lazy { Runnable { preRenderPage() } } + + //绘制图片的paint + val imagePaint by lazy { + Paint().apply { + isAntiAlias = AppConfig.useAntiAlias + } + } + + init { + callBack = activity as CallBack + } + + /** + * 设置内容 + */ + fun setContent(textPage: TextPage) { + this.textPage = textPage + // 非滑动翻页动画需要同步重绘,不然翻页可能会出现闪烁 + if (isScroll) { + postInvalidate() + } else { + invalidate() + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (!isMainView) return + ChapterProvider.upViewSize(w, h) + textPage.format() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + autoPager?.onDraw(canvas) + if (longScreenshot) { + canvas.translate(0f, scrollY.toFloat()) + } + check(!visibleRect.isEmpty) { "visibleRect 为空" } + canvas.clipRect(visibleRect) + drawPage(canvas) + } + + /** + * 绘制页面 + */ + private fun drawPage(canvas: Canvas) { + var relativeOffset = relativeOffset(0) + textPage.draw(this, canvas, relativeOffset) + if (!callBack.isScroll) return + //滚动翻页 + if (!pageFactory.hasNext()) return + val textPage1 = relativePage(1) + relativeOffset += textPage.height + textPage1.draw(this, canvas, relativeOffset) + if (!pageFactory.hasNextPlus()) return + relativeOffset += textPage1.height + if (relativeOffset < ChapterProvider.visibleHeight) { + val textPage2 = relativePage(2) + textPage2.draw(this, canvas, relativeOffset) + } + } + + override fun computeScroll() { + pageDelegate?.computeScroll() + autoPager?.computeOffset() + } + + /** + * 滚动事件 + * pageOffset 向上滚动 减小 向下滚动 增大 + * pageOffset 范围 0 ~ -textPage.height 大于0为上一页,小于-textPage.height为下一页 + * 以内容显示区域顶端为界,pageOffset的绝对值为textPage上方的高度 + * pageOffset + textPage.height 为 textPage 下方的高度 + */ + fun scroll(mOffset: Int) { + pageOffset += mOffset + if (longScreenshot) { + scrollY += -mOffset + } + if (!pageFactory.hasPrev() && pageOffset > 0) { + pageOffset = 0 + pageDelegate?.abortAnim() + } else if (!pageFactory.hasNext() + && pageOffset < 0 + && pageOffset + textPage.height < ChapterProvider.visibleHeight + ) { + val offset = (ChapterProvider.visibleHeight - textPage.height).toInt() + pageOffset = min(0, offset) + pageDelegate?.abortAnim() + } else if (pageOffset > 0) { + if (pageFactory.moveToPrev(true)) { + pageOffset -= textPage.height.toInt() + } else { + pageOffset = 0 + pageDelegate?.abortAnim() + } + } else if (pageOffset < -textPage.height) { + val height = textPage.height + if (pageFactory.moveToNext(upContent = true)) { + pageOffset += height.toInt() + } else { + pageOffset = -height.toInt() + pageDelegate?.abortAnim() + } + } + postInvalidate() + } + + fun submitRenderTask() { + renderThread.submit(renderRunnable) + } + + private fun preRenderPage() { + val view = this + var invalidate = false + pageFactory.run { + if (hasPrev() && prevPage.render(view)) { + invalidate = true + } + if (curPage.render(view)) { + invalidate = true + } + if (hasNext() && nextPage.render(view) && callBack.isScroll) { + invalidate = true + } + if (hasNextPlus() && nextPlusPage.render(view) && callBack.isScroll + && relativeOffset(2) < ChapterProvider.visibleHeight + ) { + invalidate = true + } + if (invalidate) { + postInvalidate() + pageDelegate?.postInvalidate() + } + } + } + + /** + * 重置滚动位置 + */ + fun resetPageOffset() { + pageOffset = 0 + } + + /** + * 长按 + */ + fun longPress( + x: Float, + y: Float, + select: (textPos: TextPos) -> Unit, + ) { + touch(x, y) { _, textPos, _, _, column -> + when (column) { + is ImageColumn -> callBack.onImageLongPress(x, y, column.src) + is TextColumn -> { + if (!selectAble) return@touch + column.selected = true + select(textPos) + } + } + } + } + + /** + * 单击 + * @return true:已处理, false:未处理 + */ + @Suppress("UNUSED_ANONYMOUS_PARAMETER") + fun click(x: Float, y: Float): Boolean { + var handled = false + touch(x, y) { _, textPos, textPage, textLine, column -> + when (column) { + is ButtonColumn -> { + context.toastOnUi("Button Pressed!") + handled = true + } + + is ReviewColumn -> { + context.toastOnUi("Button Pressed!") + handled = true + } + + is ImageColumn -> if (AppConfig.previewImageByClick) { + activity?.showDialogFragment(PhotoDialog(column.src)) + handled = true + } + } + } + return handled + } + + /** + * 选择文字 + */ + fun selectText( + x: Float, + y: Float, + select: (textPos: TextPos) -> Unit, + ) { + touchRough(x, y) { _, textPos, _, _, column -> + if (column is TextColumn) { + column.selected = true + select(textPos) + } + } + } + + /** + * 开始选择符移动 + */ + fun selectStartMove(x: Float, y: Float) { + touchRough(x, y) { _, textPos, _, _, _ -> + if (selectStart.compare(textPos) == 0) { + return@touchRough + } + if (textPos.compare(selectEnd) <= 0) { + selectStartMoveIndex(textPos) + } else { + touchRough(x - 2 * cursorWidth, y) { _, textPos, _, _, _ -> + if (textPos.compare(selectEnd) > 0) { + reverseStartCursor = true + reverseEndCursor = false + selectEnd.columnIndex++ + selectStartMoveIndex(selectEnd) + selectEndMoveIndex(textPos) + } + } + } + } + } + + /** + * 结束选择符移动 + */ + fun selectEndMove(x: Float, y: Float) { + touchRough(x, y) { _, textPos, _, _, _ -> + if (textPos.compare(selectEnd) == 0) { + return@touchRough + } + if (textPos.compare(selectStart) >= 0) { + selectEndMoveIndex(textPos) + } else { + touchRough(x + 2 * cursorWidth, y) { _, textPos, _, _, _ -> + if (textPos.compare(selectStart) < 0) { + reverseEndCursor = true + reverseStartCursor = false + selectStart.columnIndex-- + selectEndMoveIndex(selectStart) + selectStartMoveIndex(textPos) + } + } + } + } + } + + /** + * 触碰位置信息 + * @param touched 回调 + */ + private fun touch( + x: Float, + y: Float, + touched: ( + relativeOffset: Float, + textPos: TextPos, + textPage: TextPage, + textLine: TextLine, + column: BaseColumn + ) -> Unit + ) { + if (!visibleRect.contains(x, y)) return + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) return + if (relativeOffset >= ChapterProvider.visibleHeight) return + } + val textPage = relativePage(relativePos) + for ((lineIndex, textLine) in textPage.lines.withIndex()) { + if (textLine.isTouch(x, y, relativeOffset)) { + for ((charIndex, textColumn) in textLine.columns.withIndex()) { + if (textColumn.isTouch(x)) { + touched.invoke( + relativeOffset, + TextPos(relativePos, lineIndex, charIndex), + textPage, textLine, textColumn + ) + return + } + } + return + } + } + } + } + + /** + * 触碰位置信息 + * 文本选择专用 + * @param touched 回调 + */ + private fun touchRough( + x: Float, + y: Float, + touched: ( + relativeOffset: Float, + textPos: TextPos, + textPage: TextPage, + textLine: TextLine, + column: BaseColumn + ) -> Unit + ) { + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) return + if (relativeOffset >= ChapterProvider.visibleHeight) return + } + val textPage = relativePage(relativePos) + for (lineIndex in textPage.lines.indices) { + val textLine = textPage.getLine(lineIndex) + if (textLine.isTouchY(y, relativeOffset)) { + if (textPage.doublePage) { + val halfWidth = width / 2 + if (textLine.isLeftLine && x > halfWidth) { + continue + } + if (!textLine.isLeftLine && x < halfWidth) { + continue + } + } + val columns = textLine.columns + for (charIndex in columns.indices) { + val textColumn = columns[charIndex] + if (textColumn.isTouch(x)) { + touched.invoke( + relativeOffset, + TextPos(relativePos, lineIndex, charIndex), + textPage, textLine, textColumn + ) + return + } + } + val isLast = columns.first().start < x + val charIndex = if (isLast) columns.lastIndex + 1 else -1 + val textColumn = if (isLast) columns.last() else columns.first() + touched.invoke( + relativeOffset, + TextPos(relativePos, lineIndex, charIndex), + textPage, textLine, textColumn + ) + return + } + } + } + } + + fun getCurVisiblePage(): TextPage { + val visiblePage = TextPage() + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) break + if (relativeOffset >= ChapterProvider.visibleHeight) break + } + val textPage = relativePage(relativePos) + val lines = textPage.lines + for (i in lines.indices) { + val textLine = lines[i] + if (textLine.isVisible(relativeOffset)) { + val visibleLine = textLine.copy().apply { + lineTop += relativeOffset + lineBottom += relativeOffset + } + visiblePage.addLine(visibleLine) + } + } + } + return visiblePage + } + + fun getReadAloudPos(): Pair? { + var relativeOffset: Float + for (relativePos in 0..2) { + relativeOffset = relativeOffset(relativePos) + if (relativePos > 0) { + //滚动翻页 + if (!callBack.isScroll) break + if (relativeOffset >= ChapterProvider.visibleHeight) break + } + val textPage = relativePage(relativePos) + val lines = textPage.lines + for (i in lines.indices) { + val textLine = lines[i] + if (textLine.isVisible(relativeOffset)) { + val visibleLine = textLine.copy().apply { + lineTop += relativeOffset + lineBottom += relativeOffset + } + return textPage.chapterIndex to visibleLine + } + } + } + return null + } + + /** + * 选择开始文字 + */ + fun selectStartMoveIndex( + relativePagePos: Int, + lineIndex: Int, + charIndex: Int, + ) { + selectStart.relativePagePos = relativePagePos + selectStart.lineIndex = lineIndex + selectStart.columnIndex = max(0, charIndex) + val textLine = relativePage(relativePagePos).getLine(lineIndex) + val textColumn = textLine.getColumn(charIndex) + upSelectedStart( + if (charIndex < textLine.columns.size) textColumn.start else textColumn.end, + textLine.lineBottom + relativeOffset(relativePagePos), + textLine.lineTop + relativeOffset(relativePagePos) + ) + upSelectChars() + } + + fun selectStartMoveIndex(textPos: TextPos) = textPos.run { + selectStartMoveIndex(relativePagePos, lineIndex, columnIndex) + } + + /** + * 选择结束文字 + */ + fun selectEndMoveIndex( + relativePage: Int, + lineIndex: Int, + charIndex: Int, + ) { + selectEnd.relativePagePos = relativePage + selectEnd.lineIndex = lineIndex + val textLine = relativePage(relativePage).getLine(lineIndex) + selectEnd.columnIndex = min(charIndex, textLine.columns.lastIndex) + val textColumn = textLine.getColumn(charIndex) + upSelectedEnd( + if (charIndex > -1) textColumn.end else textColumn.start, + textLine.lineBottom + relativeOffset(relativePage) + ) + upSelectChars() + } + + fun selectEndMoveIndex(textPos: TextPos) = textPos.run { + selectEndMoveIndex(relativePagePos, lineIndex, columnIndex) + } + + private fun upSelectChars() { + if (!selectStart.isSelected() && !selectEnd.isSelected()) { + return + } + val last = if (callBack.isScroll) 2 else 0 + val textPos = TextPos(0, 0, 0) + for (relativePos in 0..last) { + textPos.relativePagePos = relativePos + val textPage = relativePage(relativePos) + for ((lineIndex, textLine) in textPage.lines.withIndex()) { + textPos.lineIndex = lineIndex + for ((charIndex, column) in textLine.columns.withIndex()) { + textPos.columnIndex = charIndex + if (column is TextColumn) { + val compareStart = textPos.compare(selectStart) + val compareEnd = textPos.compare(selectEnd) + column.selected = compareStart >= 0 && compareEnd <= 0 + column.isSearchResult = + column.selected && callBack.isSelectingSearchResult + if (column.isSearchResult) { + textPage.searchResult.add(column) + } + } + } + } + } + postInvalidate() + } + + private fun upSelectedStart(x: Float, y: Float, top: Float) { + callBack.run { + upSelectedStart(x, y + headerHeight, top + headerHeight) + } + } + + private fun upSelectedEnd(x: Float, y: Float) { + callBack.run { + upSelectedEnd(x, y + headerHeight) + } + } + + fun resetReverseCursor() { + reverseStartCursor = false + reverseEndCursor = false + } + + fun cancelSelect(clearSearchResult: Boolean = false) { + val last = if (callBack.isScroll) 2 else 0 + for (relativePos in 0..last) { + val textPage = relativePage(relativePos) + textPage.lines.forEach { textLine -> + textLine.columns.forEach { + if (it is TextColumn) { + it.selected = false + if (clearSearchResult) { + it.isSearchResult = false + textPage.searchResult.remove(it) + } + } + } + } + } + selectStart.reset() + selectEnd.reset() + postInvalidate() + callBack.onCancelSelect() + } + + fun getSelectedText(): String { + val textPos = TextPos(0, 0, 0) + val builder = StringBuilder() + for (relativePos in selectStart.relativePagePos..selectEnd.relativePagePos) { + val textPage = relativePage(relativePos) + textPos.relativePagePos = relativePos + textPage.lines.forEachIndexed { lineIndex, textLine -> + textPos.lineIndex = lineIndex + textLine.columns.forEachIndexed { charIndex, column -> + textPos.columnIndex = charIndex + val compareStart = textPos.compare(selectStart) + val compareEnd = textPos.compare(selectEnd) + if (column is TextColumn) { + when { + compareStart == -1 -> if ( + selectStart.columnIndex == textLine.columns.size + && charIndex == textLine.columns.lastIndex + ) { + builder.append("\n") + } + + compareEnd == 1 -> if (selectEnd.columnIndex == -1 && charIndex == 0) { + builder.append("\n") + } + + compareStart >= 0 && compareEnd <= 0 -> { + builder.append(column.charData) + if ( + textLine.isParagraphEnd + && charIndex == textLine.columns.lastIndex + && compareEnd != 0 + ) { + builder.append("\n") + } + } + } + } + } + } + } + return builder.toString() + } + + fun createBookmark(): Bookmark? { + val page = relativePage(selectStart.relativePagePos) + page.getTextChapter().let { chapter -> + ReadBook.book?.let { book -> + return book.createBookMark().apply { + chapterIndex = page.chapterIndex + chapterPos = chapter.getReadLength(page.index) + + page.getPosByLineColumn(selectStart.lineIndex, selectStart.columnIndex) + chapterName = chapter.title + bookText = getSelectedText() + } + } + } + return null + } + + private fun relativeOffset(relativePos: Int): Float { + return when (relativePos) { + 0 -> pageOffset.toFloat() + 1 -> pageOffset + textPage.height + else -> pageOffset + textPage.height + pageFactory.nextPage.height + } + } + + fun relativePage(relativePos: Int): TextPage { + return when (relativePos) { + 0 -> textPage + 1 -> pageFactory.nextPage + else -> pageFactory.nextPlusPage + } + } + + fun setAutoPager(autoPager: AutoPager?) { + this.autoPager = autoPager + } + + fun setIsScroll(value: Boolean) { + isScroll = value + } + + override fun canScrollVertically(direction: Int): Boolean { + return callBack.isScroll && pageFactory.hasNext() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + longScreenshot = true + scrollY = 0 + } + + MotionEvent.ACTION_UP -> { + longScreenshot = false + scrollY = 0 + } + } + return callBack.onLongScreenshotTouchEvent(event) + } + + companion object { + private val renderThread by lazy { + Executors.newSingleThreadExecutor { + Thread(it, "TextPageRender") + } + } + private val cursorWidth = 24.dpToPx() + } + + interface CallBack { + val headerHeight: Int + val pageFactory: TextPageFactory + val pageDelegate: PageDelegate? + val isScroll: Boolean + var isSelectingSearchResult: Boolean + fun upSelectedStart(x: Float, y: Float, top: Float) + fun upSelectedEnd(x: Float, y: Float) + fun onImageLongPress(x: Float, y: Float, src: String) + fun onCancelSelect() + fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt new file mode 100755 index 000000000..e343afdff --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt @@ -0,0 +1,503 @@ +package io.legado.app.ui.book.read.page + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.LayerDrawable +import android.view.LayoutInflater +import android.widget.FrameLayout +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import io.legado.app.R +import io.legado.app.constant.AppConst.timeFormat +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ViewBookPageBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.config.ReadTipConfig +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.entities.TextPos +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.widget.BatteryView +import io.legado.app.utils.activity +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.applyStatusBarPadding +import io.legado.app.utils.dpToPx +import io.legado.app.utils.gone +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.setTextIfNotEqual +import splitties.views.backgroundColor +import java.util.Date + +/** + * 页面视图 + */ +class PageView(context: Context) : FrameLayout(context) { + + private val binding = ViewBookPageBinding.inflate(LayoutInflater.from(context), this, true) + private val readBookActivity get() = activity as? ReadBookActivity + private var battery = 100 + private var tvTitle: BatteryView? = null + private var tvTime: BatteryView? = null + private var tvBattery: BatteryView? = null + private var tvBatteryP: BatteryView? = null + private var tvPage: BatteryView? = null + private var tvTotalProgress: BatteryView? = null + private var tvTotalProgress1: BatteryView? = null + private var tvPageAndTotal: BatteryView? = null + private var tvBookName: BatteryView? = null + private var tvTimeBattery: BatteryView? = null + private var tvTimeBatteryP: BatteryView? = null + private var isMainView = false + var isScroll = false + + val headerHeight: Int + get() { + val h1 = if (binding.vwStatusBar.isGone) 0 else binding.vwStatusBar.height + val h2 = if (binding.llHeader.isGone) 0 else binding.llHeader.height + return h1 + h2 + binding.vwRoot.paddingTop + } + + init { + if (!isInEditMode) { + upStyle() + binding.vwStatusBar.applyStatusBarPadding() + binding.vwNavigationBar.applyNavigationBarPadding() + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + upBg() + } + + fun upStyle() = binding.run { + upTipStyle() + ReadBookConfig.let { + val textColor = it.textColor + val tipColor = with(ReadTipConfig) { + if (tipColor == 0) textColor else tipColor + } + val tipDividerColor = with(ReadTipConfig) { + when (tipDividerColor) { + -1 -> ContextCompat.getColor(context, R.color.divider) + 0 -> textColor + else -> tipDividerColor + } + } + tvHeaderLeft.setColor(tipColor) + tvHeaderMiddle.setColor(tipColor) + tvHeaderRight.setColor(tipColor) + tvFooterLeft.setColor(tipColor) + tvFooterMiddle.setColor(tipColor) + tvFooterRight.setColor(tipColor) + vwTopDivider.backgroundColor = tipDividerColor + vwBottomDivider.backgroundColor = tipDividerColor + upStatusBar() + upNavigationBar() + upPaddingDisplayCutouts() + llHeader.setPadding( + it.headerPaddingLeft.dpToPx(), + it.headerPaddingTop.dpToPx(), + it.headerPaddingRight.dpToPx(), + it.headerPaddingBottom.dpToPx() + ) + llFooter.setPadding( + it.footerPaddingLeft.dpToPx(), + it.footerPaddingTop.dpToPx(), + it.footerPaddingRight.dpToPx(), + it.footerPaddingBottom.dpToPx() + ) + vwTopDivider.gone(llHeader.isGone || !it.showHeaderLine) + vwBottomDivider.gone(llFooter.isGone || !it.showFooterLine) + } + upTime() + upBattery(battery) + } + + /** + * 显示状态栏时隐藏header + */ + fun upStatusBar() = with(binding.vwStatusBar) { +// setPadding(paddingLeft, context.statusBarHeight, paddingRight, paddingBottom) + isGone = ReadBookConfig.hideStatusBar || readBookActivity?.isInMultiWindow == true + } + + fun upNavigationBar() { + binding.vwNavigationBar.isGone = ReadBookConfig.hideNavigationBar + } + + fun upPaddingDisplayCutouts() { + if (AppConfig.paddingDisplayCutouts) { + binding.vwRoot.setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + binding.vwRoot.setPadding( + insets.left, + if (binding.vwStatusBar.isGone) insets.top else 0, + insets.right, + insets.bottom + ) + windowInsets + } + } else { + ViewCompat.setOnApplyWindowInsetsListener(binding.vwRoot, null) + binding.vwRoot.setPadding(0, 0, 0, 0) + } + } + + /** + * 更新阅读信息 + */ + private fun upTipStyle() = binding.run { + tvHeaderLeft.tag = null + tvHeaderMiddle.tag = null + tvHeaderRight.tag = null + tvFooterLeft.tag = null + tvFooterMiddle.tag = null + tvFooterRight.tag = null + llHeader.isGone = when (ReadTipConfig.headerMode) { + 1 -> false + 2 -> true + else -> !ReadBookConfig.hideStatusBar + } + llFooter.isGone = when (ReadTipConfig.footerMode) { + 1 -> true + else -> false + } + ReadTipConfig.apply { + tvHeaderLeft.isGone = tipHeaderLeft == none + tvHeaderRight.isGone = tipHeaderRight == none + tvHeaderMiddle.isGone = tipHeaderMiddle == none + tvFooterLeft.isInvisible = tipFooterLeft == none + tvFooterRight.isGone = tipFooterRight == none + tvFooterMiddle.isGone = tipFooterMiddle == none + } + tvTitle = getTipView(ReadTipConfig.chapterTitle)?.apply { + tag = ReadTipConfig.chapterTitle + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTime = getTipView(ReadTipConfig.time)?.apply { + tag = ReadTipConfig.time + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvBattery = getTipView(ReadTipConfig.battery)?.apply { + tag = ReadTipConfig.battery + isBattery = true + textSize = 11f + } + tvPage = getTipView(ReadTipConfig.page)?.apply { + tag = ReadTipConfig.page + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTotalProgress = getTipView(ReadTipConfig.totalProgress)?.apply { + tag = ReadTipConfig.totalProgress + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTotalProgress1 = getTipView(ReadTipConfig.totalProgress1)?.apply { + tag = ReadTipConfig.totalProgress1 + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvPageAndTotal = getTipView(ReadTipConfig.pageAndTotal)?.apply { + tag = ReadTipConfig.pageAndTotal + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvBookName = getTipView(ReadTipConfig.bookName)?.apply { + tag = ReadTipConfig.bookName + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTimeBattery = getTipView(ReadTipConfig.timeBattery)?.apply { + tag = ReadTipConfig.timeBattery + isBattery = true + typeface = ChapterProvider.typeface + textSize = 11f + } + tvBatteryP = getTipView(ReadTipConfig.batteryPercentage)?.apply { + tag = ReadTipConfig.batteryPercentage + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + tvTimeBatteryP = getTipView(ReadTipConfig.timeBatteryPercentage)?.apply { + tag = ReadTipConfig.timeBatteryPercentage + isBattery = false + typeface = ChapterProvider.typeface + textSize = 12f + } + } + + /** + * 获取信息视图 + * @param tip 信息类型 + */ + private fun getTipView(tip: Int): BatteryView? = binding.run { + return when (tip) { + ReadTipConfig.tipHeaderLeft -> tvHeaderLeft + ReadTipConfig.tipHeaderMiddle -> tvHeaderMiddle + ReadTipConfig.tipHeaderRight -> tvHeaderRight + ReadTipConfig.tipFooterLeft -> tvFooterLeft + ReadTipConfig.tipFooterMiddle -> tvFooterMiddle + ReadTipConfig.tipFooterRight -> tvFooterRight + else -> null + } + } + + /** + * 更新背景 + */ + fun upBg() { + binding.vwRoot.background = LayerDrawable( + arrayOf( + ReadBookConfig.bgMeanColor.toDrawable(), + ReadBookConfig.bg + ) + ) + upBgAlpha() + } + + /** + * 更新背景透明度 + */ + fun upBgAlpha() { + ReadBookConfig.bg?.alpha = (ReadBookConfig.bgAlpha / 100f * 255).toInt() + binding.vwRoot.invalidate() + } + + /** + * 更新时间信息 + */ + fun upTime() { + tvTime?.text = timeFormat.format(Date(System.currentTimeMillis())) + upTimeBattery() + } + + /** + * 更新电池信息 + */ + @SuppressLint("SetTextI18n") + fun upBattery(battery: Int) { + this.battery = battery + tvBattery?.setBattery(battery) + tvBatteryP?.text = "$battery%" + upTimeBattery() + } + + /** + * 更新电池信息 + */ + @SuppressLint("SetTextI18n") + private fun upTimeBattery() { + val time = timeFormat.format(Date(System.currentTimeMillis())) + tvTimeBattery?.setBattery(battery, time) + tvTimeBatteryP?.text = "$time $battery%" + } + + /** + * 设置内容 + */ + fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { + if (isMainView && !isScroll) { + setProgress(textPage) + } else { + post { + setProgress(textPage) + } + } + if (resetPageOffset) { + resetPageOffset() + } + binding.contentTextView.setContent(textPage) + } + + fun invalidateContentView() { + binding.contentTextView.invalidate() + } + + /** + * 设置无障碍文本 + */ + fun setContentDescription(content: String) { + binding.contentTextView.contentDescription = content + } + + /** + * 重置滚动位置 + */ + fun resetPageOffset() { + binding.contentTextView.resetPageOffset() + } + + /** + * 设置进度 + */ + @SuppressLint("SetTextI18n") + fun setProgress(textPage: TextPage) = textPage.apply { + tvBookName?.setTextIfNotEqual(ReadBook.book?.name) + tvTitle?.setTextIfNotEqual(textPage.title) + val readProgress = readProgress + tvTotalProgress?.setTextIfNotEqual(readProgress) + tvTotalProgress1?.setTextIfNotEqual("${chapterIndex.plus(1)}/${chapterSize}") + if (textChapter.isCompleted) { + tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") + tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") + } else { + val pageSizeInt = pageSize + val pageSize = if (pageSizeInt <= 0) "-" else "~$pageSizeInt" + tvPageAndTotal?.setTextIfNotEqual("${index.plus(1)}/$pageSize $readProgress") + tvPage?.setTextIfNotEqual("${index.plus(1)}/$pageSize") + } + } + + fun setAutoPager(autoPager: AutoPager?) { + binding.contentTextView.setAutoPager(autoPager) + } + + fun submitRenderTask() { + binding.contentTextView.submitRenderTask() + } + + fun setIsScroll(value: Boolean) { + isScroll = value + binding.contentTextView.setIsScroll(value) + } + + /** + * 滚动事件 + */ + fun scroll(offset: Int) { + binding.contentTextView.scroll(offset) + } + + /** + * 更新是否开启选择功能 + */ + fun upSelectAble(selectAble: Boolean) { + binding.contentTextView.selectAble = selectAble + } + + /** + * 优先处理页面内单击 + * @return true:已处理, false:未处理 + */ + fun onClick(x: Float, y: Float): Boolean { + return binding.contentTextView.click(x, y - headerHeight) + } + + /** + * 长按事件 + */ + fun longPress( + x: Float, y: Float, + select: (textPos: TextPos) -> Unit, + ) { + return binding.contentTextView.longPress(x, y - headerHeight, select) + } + + /** + * 选择文本 + */ + fun selectText( + x: Float, y: Float, + select: (textPos: TextPos) -> Unit, + ) { + return binding.contentTextView.selectText(x, y - headerHeight, select) + } + + fun getCurVisiblePage(): TextPage { + return binding.contentTextView.getCurVisiblePage() + } + + fun getReadAloudPos(): Pair? { + return binding.contentTextView.getReadAloudPos() + } + + fun markAsMainView() { + isMainView = true + binding.contentTextView.isMainView = true + } + + fun selectStartMove(x: Float, y: Float) { + binding.contentTextView.selectStartMove(x, y - headerHeight) + } + + fun selectStartMoveIndex( + relativePagePos: Int, + lineIndex: Int, + charIndex: Int + ) { + binding.contentTextView.selectStartMoveIndex(relativePagePos, lineIndex, charIndex) + } + + fun selectStartMoveIndex(textPos: TextPos) { + binding.contentTextView.selectStartMoveIndex(textPos) + } + + fun selectEndMove(x: Float, y: Float) { + binding.contentTextView.selectEndMove(x, y - headerHeight) + } + + fun selectEndMoveIndex( + relativePagePos: Int, + lineIndex: Int, + charIndex: Int + ) { + binding.contentTextView.selectEndMoveIndex(relativePagePos, lineIndex, charIndex) + } + + fun selectEndMoveIndex(textPos: TextPos) { + binding.contentTextView.selectEndMoveIndex(textPos) + } + + fun getReverseStartCursor(): Boolean { + return binding.contentTextView.reverseStartCursor + } + + fun getReverseEndCursor(): Boolean { + return binding.contentTextView.reverseEndCursor + } + + fun isLongScreenShot(): Boolean { + return binding.contentTextView.longScreenshot + } + + fun resetReverseCursor() { + binding.contentTextView.resetReverseCursor() + } + + fun cancelSelect(clearSearchResult: Boolean = false) { + binding.contentTextView.cancelSelect(clearSearchResult) + } + + fun createBookmark(): Bookmark? { + return binding.contentTextView.createBookmark() + } + + fun relativePage(relativePagePos: Int): TextPage { + return binding.contentTextView.relativePage(relativePagePos) + } + + val textPage get() = binding.contentTextView.textPage + + val selectedText: String get() = binding.contentTextView.getSelectedText() + + val selectStartPos get() = binding.contentTextView.selectStart +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt new file mode 100755 index 000000000..6063076ee --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt @@ -0,0 +1,758 @@ +package io.legado.app.ui.book.read.page + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.RectF +import android.os.Build +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.ViewConfiguration +import android.view.WindowInsets +import android.widget.FrameLayout +import io.legado.app.R +import io.legado.app.constant.PageAnim +import io.legado.app.data.entities.BookProgress +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.model.ReadAloud +import io.legado.app.model.ReadBook +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.book.read.ContentEditDialog +import io.legado.app.ui.book.read.page.api.DataSource +import io.legado.app.ui.book.read.page.delegate.CoverPageDelegate +import io.legado.app.ui.book.read.page.delegate.HorizontalPageDelegate +import io.legado.app.ui.book.read.page.delegate.NoAnimPageDelegate +import io.legado.app.ui.book.read.page.delegate.PageDelegate +import io.legado.app.ui.book.read.page.delegate.ScrollPageDelegate +import io.legado.app.ui.book.read.page.delegate.SimulationPageDelegate +import io.legado.app.ui.book.read.page.delegate.SlidePageDelegate +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.entities.TextPos +import io.legado.app.ui.book.read.page.entities.column.TextColumn +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.LayoutProgressListener +import io.legado.app.ui.book.read.page.provider.TextPageFactory +import io.legado.app.utils.activity +import io.legado.app.utils.invisible +import io.legado.app.utils.longToastOnUi +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.throttle +import java.text.BreakIterator +import java.util.Locale +import kotlin.math.abs + +/** + * 阅读视图 + */ +class ReadView(context: Context, attrs: AttributeSet) : + FrameLayout(context, attrs), + DataSource, LayoutProgressListener { + + val callBack: CallBack get() = activity as CallBack + var pageFactory: TextPageFactory = TextPageFactory(this) + var pageDelegate: PageDelegate? = null + private set(value) { + field?.onDestroy() + field = null + field = value + upContent() + } + override var isScroll = false + val prevPage by lazy { PageView(context) } + val curPage by lazy { PageView(context) } + val nextPage by lazy { PageView(context) } + val defaultAnimationSpeed = 300 + private var pressDown = false + private var isMove = false + + //起始点 + var startX: Float = 0f + var startY: Float = 0f + + //上一个触碰点 + var lastX: Float = 0f + var lastY: Float = 0f + + //触碰点 + var touchX: Float = 0f + var touchY: Float = 0f + + //是否停止动画动作 + var isAbortAnim = false + + //长按 + private var longPressed = false + private val longPressTimeout = 600L + private val longPressRunnable = Runnable { + longPressed = true + onLongPress() + } + var isTextSelected = false + private var pressOnTextSelected = false + private val initialTextPos = TextPos(0, 0, 0) + + private val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop } + private var pageSlopSquare: Int = slopSquare + var pageSlopSquare2: Int = pageSlopSquare * pageSlopSquare + private val tlRect = RectF() + private val tcRect = RectF() + private val trRect = RectF() + private val mlRect = RectF() + private val mcRect = RectF() + private val mrRect = RectF() + private val blRect = RectF() + private val bcRect = RectF() + private val brRect = RectF() + private val boundary by lazy { BreakIterator.getWordInstance(Locale.getDefault()) } + private val upProgressThrottle = throttle(200) { post { upProgress() } } + val autoPager = AutoPager(this) + val isAutoPage get() = autoPager.isRunning + + init { + addView(nextPage) + addView(curPage) + addView(prevPage) + prevPage.invisible() + nextPage.invisible() + curPage.markAsMainView() + if (!isInEditMode) { + upBg() + setWillNotDraw(false) + upPageAnim() + upPageSlopSquare() + } + } + + private fun setRect9x() { + tlRect.set(0f, 0f, width * 0.33f, height * 0.33f) + tcRect.set(width * 0.33f, 0f, width * 0.66f, height * 0.33f) + trRect.set(width * 0.36f, 0f, width.toFloat(), height * 0.33f) + mlRect.set(0f, height * 0.33f, width * 0.33f, height * 0.66f) + mcRect.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f) + mrRect.set(width * 0.66f, height * 0.33f, width.toFloat(), height * 0.66f) + blRect.set(0f, height * 0.66f, width * 0.33f, height.toFloat()) + bcRect.set(width * 0.33f, height * 0.66f, width * 0.66f, height.toFloat()) + brRect.set(width * 0.66f, height * 0.66f, width.toFloat(), height.toFloat()) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + setRect9x() + prevPage.x = -w.toFloat() + pageDelegate?.setViewSize(w, h) + if (w > 0 && h > 0) { + upBg() + callBack.upSystemUiVisibility() + } + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + pageDelegate?.onDraw(canvas) + autoPager.onDraw(canvas) + } + + override fun computeScroll() { + pageDelegate?.computeScroll() + autoPager.computeOffset() + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + return true + } + + /** + * 触摸事件 + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val insets = this.rootWindowInsets.getInsetsIgnoringVisibility( + WindowInsets.Type.mandatorySystemGestures() + ) + val height = activity?.windowManager?.currentWindowMetrics?.bounds?.height() + if (height != null) { + if (event.y > height.minus(insets.bottom) + && event.action != MotionEvent.ACTION_UP + && event.action != MotionEvent.ACTION_CANCEL + ) { + return true + } + } + } + + //在多点触控时,事件不走ACTION_DOWN分支而产生的特殊事件处理 + if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN || event.actionMasked == MotionEvent.ACTION_POINTER_UP) { + pageDelegate?.onTouch(event) + } + when (event.action) { + MotionEvent.ACTION_DOWN -> { + callBack.screenOffTimerStart() + if (isTextSelected) { + curPage.cancelSelect() + isTextSelected = false + pressOnTextSelected = true + } else { + pressOnTextSelected = false + } + longPressed = false + postDelayed(longPressRunnable, longPressTimeout) + pressDown = true + isMove = false + pageDelegate?.onTouch(event) + pageDelegate?.onDown() + setStartPoint(event.x, event.y, false) + } + + MotionEvent.ACTION_MOVE -> { + if (!pressDown) return true + val absX = abs(startX - event.x) + val absY = abs(startY - event.y) + if (!isMove) { + isMove = absX > slopSquare || absY > slopSquare + } + if (isMove) { + longPressed = false + removeCallbacks(longPressRunnable) + if (isTextSelected) { + selectText(event.x, event.y) + } else { + pageDelegate?.onTouch(event) + } + } + } + + MotionEvent.ACTION_UP -> { + callBack.screenOffTimerStart() + removeCallbacks(longPressRunnable) + if (!pressDown) return true + pressDown = false + if (!pageDelegate!!.isMoved && !isMove) { + if (!longPressed && !pressOnTextSelected) { + if (!curPage.onClick(startX, startY)) { + onSingleTapUp() + } + return true + } + } + if (isTextSelected) { + callBack.showTextActionMenu() + } else if (pageDelegate!!.isMoved) { + pageDelegate?.onTouch(event) + } + pressOnTextSelected = false + } + + MotionEvent.ACTION_CANCEL -> { + removeCallbacks(longPressRunnable) + if (!pressDown) return true + pressDown = false + if (isTextSelected) { + callBack.showTextActionMenu() + } else if (pageDelegate!!.isMoved) { + pageDelegate?.onTouch(event) + } + pressOnTextSelected = false + autoPager.resume() + } + } + return true + } + + fun cancelSelect(clearSearchResult: Boolean = false) { + if (isTextSelected) { + curPage.cancelSelect(clearSearchResult) + isTextSelected = false + } + } + + /** + * 更新状态栏 + */ + fun upStatusBar() { + curPage.upStatusBar() + prevPage.upStatusBar() + nextPage.upStatusBar() + } + + /** + * 保存开始位置 + */ + fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) { + startX = x + startY = y + lastX = x + lastY = y + touchX = x + touchY = y + + if (invalidate) { + invalidate() + } + } + + /** + * 保存当前位置 + */ + fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { + lastX = touchX + lastY = touchY + touchX = x + touchY = y + if (invalidate) { + invalidate() + } + pageDelegate?.onScroll() + val offset = touchY - lastY + touchY -= offset - offset.toInt() + } + + /** + * 长按选择 + */ + private fun onLongPress() { + kotlin.runCatching { + curPage.longPress(startX, startY) { textPos: TextPos -> + isTextSelected = true + pressOnTextSelected = true + initialTextPos.upData(textPos) + val startPos = textPos.copy() + val endPos = textPos.copy() + val page = curPage.relativePage(textPos.relativePagePos) + val stringBuilder = StringBuilder() + var cIndex = textPos.columnIndex + var lineStart = textPos.lineIndex + var lineEnd = textPos.lineIndex + for (index in textPos.lineIndex - 1 downTo 0) { + val textLine = page.getLine(index) + if (textLine.isParagraphEnd) { + break + } else { + stringBuilder.insert(0, textLine.text) + lineStart -= 1 + cIndex += textLine.charSize + } + } + for (index in textPos.lineIndex until page.lineSize) { + val textLine = page.getLine(index) + stringBuilder.append(textLine.text) + lineEnd += 1 + if (textLine.isParagraphEnd) { + break + } + } + var start: Int + var end: Int + boundary.setText(stringBuilder.toString()) + start = boundary.first() + end = boundary.next() + while (end != BreakIterator.DONE) { + if (cIndex in start until end) { + break + } + start = end + end = boundary.next() + } + kotlin.run { + var ci = 0 + for (index in lineStart..lineEnd) { + val textLine = page.getLine(index) + for (j in textLine.columns.indices) { + if (ci == start) { + startPos.lineIndex = index + startPos.columnIndex = j + } else if (ci == end - 1) { + endPos.lineIndex = index + endPos.columnIndex = j + return@run + } + val column = textLine.getColumn(j) + if (column is TextColumn) { + ci += column.charData.length + } else { + ci++ + } + } + } + } + curPage.selectStartMoveIndex(startPos) + curPage.selectEndMoveIndex(endPos) + } + } + } + + /** + * 单击 + */ + private fun onSingleTapUp() { + when { + isTextSelected -> Unit + mcRect.contains(startX, startY) -> if (!isAbortAnim) { + click(AppConfig.clickActionMC) + } + + bcRect.contains(startX, startY) -> { + click(AppConfig.clickActionBC) + } + + blRect.contains(startX, startY) -> { + click(AppConfig.clickActionBL) + } + + brRect.contains(startX, startY) -> { + click(AppConfig.clickActionBR) + } + + mlRect.contains(startX, startY) -> { + click(AppConfig.clickActionML) + } + + mrRect.contains(startX, startY) -> { + click(AppConfig.clickActionMR) + } + + tlRect.contains(startX, startY) -> { + click(AppConfig.clickActionTL) + } + + tcRect.contains(startX, startY) -> { + click(AppConfig.clickActionTC) + } + + trRect.contains(startX, startY) -> { + click(AppConfig.clickActionTR) + } + } + } + + /** + * 点击 + */ + private fun click(action: Int) { + when (action) { + 0 -> { + pageDelegate?.dismissSnackBar() + callBack.showActionMenu() + } + + 1 -> pageDelegate?.nextPageByAnim(defaultAnimationSpeed) + 2 -> pageDelegate?.prevPageByAnim(defaultAnimationSpeed) + 3 -> ReadBook.moveToNextChapter(true) + 4 -> ReadBook.moveToPrevChapter(upContent = true, toLast = false) + 5 -> ReadAloud.prevParagraph(context) + 6 -> ReadAloud.nextParagraph(context) + 7 -> callBack.addBookmark() + 8 -> activity?.showDialogFragment(ContentEditDialog()) + 9 -> callBack.changeReplaceRuleState() + 10 -> callBack.openChapterList() + 11 -> callBack.openSearchActivity(null) + 12 -> ReadBook.syncProgress( + { progress -> callBack.sureNewProgress(progress) }, + { context.longToastOnUi(context.getString(R.string.upload_book_success)) }, + { context.longToastOnUi(context.getString(R.string.sync_book_progress_success)) }) + + 13 -> { + if (BaseReadAloudService.isPlay()) { + ReadAloud.pause(context) + } else { + ReadAloud.resume(context) + } + } + } + } + + /** + * 选择文本 + */ + private fun selectText(x: Float, y: Float) { + curPage.selectText(x, y) { textPos -> + val compare = initialTextPos.compare(textPos) + when { + compare > 0 -> { + curPage.selectStartMoveIndex(textPos) + curPage.selectEndMoveIndex( + initialTextPos.relativePagePos, + initialTextPos.lineIndex, + initialTextPos.columnIndex - 1 + ) + } + + else -> { + curPage.selectStartMoveIndex(initialTextPos) + curPage.selectEndMoveIndex(textPos) + } + } + } + } + + /** + * 销毁事件 + */ + fun onDestroy() { + pageDelegate?.onDestroy() + curPage.cancelSelect() + invalidateTextPage() + } + + /** + * 翻页动画完成后事件 + * @param direction 翻页方向 + */ + fun fillPage(direction: PageDirection): Boolean { + return when (direction) { + PageDirection.PREV -> { + pageFactory.moveToPrev(true) + } + + PageDirection.NEXT -> { + pageFactory.moveToNext(true) + } + + else -> false + } + } + + /** + * 更新翻页动画 + */ + fun upPageAnim(upRecorder: Boolean = false) { + isScroll = ReadBook.pageAnim() == 3 + ChapterProvider.upLayout() + when (ReadBook.pageAnim()) { + PageAnim.coverPageAnim -> if (pageDelegate !is CoverPageDelegate) { + pageDelegate = CoverPageDelegate(this) + } + + PageAnim.slidePageAnim -> if (pageDelegate !is SlidePageDelegate) { + pageDelegate = SlidePageDelegate(this) + } + + PageAnim.simulationPageAnim -> if (pageDelegate !is SimulationPageDelegate) { + pageDelegate = SimulationPageDelegate(this) + } + + PageAnim.scrollPageAnim -> if (pageDelegate !is ScrollPageDelegate) { + pageDelegate = ScrollPageDelegate(this) + } + + else -> if (pageDelegate !is NoAnimPageDelegate) { + pageDelegate = NoAnimPageDelegate(this) + } + } + (pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage + if (upRecorder) { + (pageDelegate as? HorizontalPageDelegate)?.upRecorder() + autoPager.upRecorder() + } + pageDelegate?.setViewSize(width, height) + if (isScroll) { + curPage.setAutoPager(autoPager) + } else { + curPage.setAutoPager(null) + } + curPage.setIsScroll(isScroll) + } + + /** + * 更新阅读内容 + * @param relativePosition 相对位置 -1 上一页 0 当前页 1 下一页 + * @param resetPageOffset 滚动阅读是是否重置位置 + */ + override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { + post { + curPage.setContentDescription(pageFactory.curPage.text) + } + if (isScroll && !isAutoPage) { + if (relativePosition == 0) { + curPage.setContent(pageFactory.curPage, resetPageOffset) + } else { + curPage.invalidateContentView() + } + } else { + when (relativePosition) { + -1 -> prevPage.setContent(pageFactory.prevPage) + 1 -> nextPage.setContent(pageFactory.nextPage) + else -> { + curPage.setContent(pageFactory.curPage, resetPageOffset) + nextPage.setContent(pageFactory.nextPage) + prevPage.setContent(pageFactory.prevPage) + } + } + } + callBack.screenOffTimerStart() + } + + private fun upProgress() { + curPage.setProgress(pageFactory.curPage) + } + + /** + * 更新滑动距离 + */ + fun upPageSlopSquare() { + val pageTouchSlop = AppConfig.pageTouchSlop + this.pageSlopSquare = if (pageTouchSlop == 0) slopSquare else pageTouchSlop + pageSlopSquare2 = this.pageSlopSquare * this.pageSlopSquare + } + + /** + * 更新样式 + */ + fun upStyle() { + ChapterProvider.upStyle() + curPage.upStyle() + prevPage.upStyle() + nextPage.upStyle() + } + + /** + * 更新背景 + */ + fun upBg() { + ReadBookConfig.upBg(width, height) + curPage.upBg() + prevPage.upBg() + nextPage.upBg() + } + + /** + * 更新背景透明度 + */ + fun upBgAlpha() { + curPage.upBgAlpha() + prevPage.upBgAlpha() + nextPage.upBgAlpha() + } + + /** + * 更新时间信息 + */ + fun upTime() { + curPage.upTime() + prevPage.upTime() + nextPage.upTime() + } + + /** + * 更新电量信息 + */ + fun upBattery(battery: Int) { + curPage.upBattery(battery) + prevPage.upBattery(battery) + nextPage.upBattery(battery) + } + + /** + * 从选择位置开始朗读 + */ + suspend fun aloudStartSelect() { + val selectStartPos = curPage.selectStartPos + var pagePos = selectStartPos.relativePagePos + val line = selectStartPos.lineIndex + val column = selectStartPos.columnIndex + while (pagePos > 0) { + if (!ReadBook.moveToNextPage()) { + ReadBook.moveToNextChapterAwait(false) + } + pagePos-- + } + val startPos = curPage.textPage.getPosByLineColumn(line, column) + ReadBook.readAloud(startPos = startPos) + } + + /** + * @return 选择的文本 + */ + fun getSelectText(): String { + return curPage.selectedText + } + + fun getCurVisiblePage(): TextPage { + return curPage.getCurVisiblePage() + } + + fun getReadAloudPos(): Pair? { + return curPage.getReadAloudPos() + } + + fun invalidateTextPage() { + if (!AppConfig.optimizeRender) { + return + } + pageFactory.run { + prevPage.invalidateAll() + curPage.invalidateAll() + nextPage.invalidateAll() + nextPlusPage.invalidateAll() + } + } + + fun onScrollAnimStart() { + autoPager.pause() + } + + fun onScrollAnimStop() { + autoPager.resume() + } + + fun onPageChange() { + autoPager.reset() + submitRenderTask() + } + + fun submitRenderTask() { + if (!AppConfig.optimizeRender) { + return + } + curPage.submitRenderTask() + } + + fun isLongScreenShot(): Boolean { + return curPage.isLongScreenShot() + } + + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + upProgressThrottle.invoke() + } + + override val currentChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(0) else null + } + + override val nextChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(1) else null + } + + override val prevChapter: TextChapter? + get() { + return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null + } + + override fun hasNextChapter(): Boolean { + return ReadBook.durChapterIndex < ReadBook.simulatedChapterSize - 1 + } + + override fun hasPrevChapter(): Boolean { + return ReadBook.durChapterIndex > 0 + } + + interface CallBack { + val isInitFinish: Boolean + fun showActionMenu() + fun screenOffTimerStart() + fun showTextActionMenu() + fun autoPageStop() + fun openChapterList() + fun addBookmark() + fun changeReplaceRuleState() + fun openSearchActivity(searchWord: String?) + fun upSystemUiVisibility() + fun sureNewProgress(progress: BookProgress) + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt new file mode 100755 index 000000000..ceddd279b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt @@ -0,0 +1,24 @@ +package io.legado.app.ui.book.read.page.api + +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.entities.TextChapter + +interface DataSource { + + val pageIndex: Int get() = ReadBook.durPageIndex + + val currentChapter: TextChapter? + + val nextChapter: TextChapter? + + val prevChapter: TextChapter? + + val isScroll: Boolean + + fun hasNextChapter(): Boolean + + fun hasPrevChapter(): Boolean + + fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt new file mode 100755 index 000000000..ecf0e56a7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/api/PageFactory.kt @@ -0,0 +1,26 @@ +package io.legado.app.ui.book.read.page.api + +abstract class PageFactory(protected val dataSource: DataSource) { + + abstract fun moveToFirst() + + abstract fun moveToLast() + + abstract fun moveToNext(upContent: Boolean): Boolean + + abstract fun moveToPrev(upContent: Boolean): Boolean + + abstract val nextPage: DATA + + abstract val prevPage: DATA + + abstract val curPage: DATA + + abstract val nextPlusPage: DATA + + abstract fun hasNext(): Boolean + + abstract fun hasPrev(): Boolean + + abstract fun hasNextPlus(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt new file mode 100755 index 000000000..5e8303312 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt @@ -0,0 +1,117 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.graphics.Canvas +import android.graphics.drawable.GradientDrawable +import androidx.core.graphics.withClip +import androidx.core.graphics.withTranslation +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.utils.screenshot + +class CoverPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { + private val shadowDrawableR: GradientDrawable + + init { + val shadowColors = intArrayOf(0x66111111, 0x00000000) + shadowDrawableR = GradientDrawable( + GradientDrawable.Orientation.LEFT_RIGHT, shadowColors + ) + shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT + } + + override fun onDraw(canvas: Canvas) { + if (!isRunning) return + val offsetX = touchX - startX + + if ((mDirection == PageDirection.NEXT && offsetX > 0) + || (mDirection == PageDirection.PREV && offsetX < 0) + ) { + return + } + + val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth + if (mDirection == PageDirection.PREV) { + if (offsetX <= viewWidth) { + canvas.withTranslation(distanceX) { + prevRecorder.draw(canvas) + } + addShadow(distanceX, canvas) + } else { + prevRecorder.draw(canvas) + } + } else if (mDirection == PageDirection.NEXT) { + val width = nextRecorder.width.toFloat() + val height = nextRecorder.height.toFloat() + canvas.withClip(width + offsetX, 0f, width, height) { + nextRecorder.draw(this) + } + canvas.withTranslation(distanceX - viewWidth) { + curRecorder.draw(this) + } + addShadow(distanceX, canvas) + } + } + + override fun setBitmap() { + when (mDirection) { + PageDirection.PREV -> { + prevPage.screenshot(prevRecorder) + } + + PageDirection.NEXT -> { + nextPage.screenshot(nextRecorder) + curPage.screenshot(curRecorder) + } + + else -> Unit + } + } + + private fun addShadow(left: Float, canvas: Canvas) { + if (left == 0f) return + val dx = if (left < 0) { + left + viewWidth + } else { + left + } + canvas.withTranslation(dx) { + shadowDrawableR.draw(canvas) + } + } + + override fun setViewSize(width: Int, height: Int) { + super.setViewSize(width, height) + shadowDrawableR.setBounds(0, 0, 30, viewHeight) + } + + override fun onAnimStop() { + if (!isCancel) { + readView.fillPage(mDirection) + } + } + + override fun onAnimStart(animationSpeed: Int) { + val distanceX: Float + when (mDirection) { + PageDirection.NEXT -> distanceX = + if (isCancel) { + var dis = viewWidth - startX + touchX + if (dis > viewWidth) { + dis = viewWidth.toFloat() + } + viewWidth - dis + } else { + -(touchX + (viewWidth - startX)) + } + + else -> distanceX = + if (isCancel) { + -(touchX - startX) + } else { + viewWidth - (touchX - startX) + } + } + startScroll(touchX.toInt(), 0, distanceX.toInt(), 0, animationSpeed) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt new file mode 100755 index 000000000..f6a4e3c3f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt @@ -0,0 +1,155 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.view.MotionEvent +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.screenshot + +abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readView) { + + protected var curRecorder = CanvasRecorderFactory.create() + protected var prevRecorder = CanvasRecorderFactory.create() + protected var nextRecorder = CanvasRecorderFactory.create() + private val slopSquare get() = readView.pageSlopSquare2 + + override fun setDirection(direction: PageDirection) { + super.setDirection(direction) + setBitmap() + } + + open fun setBitmap() { + when (mDirection) { + PageDirection.PREV -> { + prevPage.screenshot(prevRecorder) + curPage.screenshot(curRecorder) + } + + PageDirection.NEXT -> { + nextPage.screenshot(nextRecorder) + curPage.screenshot(curRecorder) + } + + else -> Unit + } + } + + fun upRecorder() { + curRecorder.recycle() + prevRecorder.recycle() + nextRecorder.recycle() + curRecorder = CanvasRecorderFactory.create() + prevRecorder = CanvasRecorderFactory.create() + nextRecorder = CanvasRecorderFactory.create() + } + + override fun onTouch(event: MotionEvent) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + abortAnim() + } + + MotionEvent.ACTION_MOVE -> { + onScroll(event) + } + + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + onAnimStart(readView.defaultAnimationSpeed) + } + } + } + + private fun onScroll(event: MotionEvent) { + val action: Int = event.action + val pointerUp = + action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_POINTER_UP + val skipIndex = if (pointerUp) event.actionIndex else -1 + // Determine focal point + var sumX = 0f + var sumY = 0f + val count: Int = event.pointerCount + for (i in 0 until count) { + if (skipIndex == i) continue + sumX += event.getX(i) + sumY += event.getY(i) + } + val div = if (pointerUp) count - 1 else count + val focusX = sumX / div + val focusY = sumY / div + //判断是否移动了 + if (!isMoved) { + val deltaX = (focusX - startX).toInt() + val deltaY = (focusY - startY).toInt() + val distance = deltaX * deltaX + deltaY * deltaY + isMoved = distance > slopSquare + if (isMoved) { + if (sumX - startX > 0) { + //如果上一页不存在 + if (!hasPrev()) { + noNext = true + return + } + setDirection(PageDirection.PREV) + } else { + //如果不存在表示没有下一页了 + if (!hasNext()) { + noNext = true + return + } + setDirection(PageDirection.NEXT) + } + readView.setStartPoint(event.x, event.y, false) + } + } + if (isMoved) { + isCancel = if (mDirection == PageDirection.NEXT) sumX > lastX else sumX < lastX + isRunning = true + //设置触摸点 + readView.setTouchPoint(sumX, sumY) + } + } + + override fun abortAnim() { + isStarted = false + isMoved = false + isRunning = false + if (!scroller.isFinished) { + readView.isAbortAnim = true + scroller.abortAnimation() + if (!isCancel) { + readView.fillPage(mDirection) + readView.invalidate() + } + } else { + readView.isAbortAnim = false + } + } + + override fun nextPageByAnim(animationSpeed: Int) { + abortAnim() + if (!hasNext()) return + setDirection(PageDirection.NEXT) + val y = when { + startY > viewHeight / 2 -> viewHeight.toFloat() * 0.9f + else -> 1f + } + readView.setStartPoint(viewWidth.toFloat() * 0.9f, y, false) + onAnimStart(animationSpeed) + } + + override fun prevPageByAnim(animationSpeed: Int) { + abortAnim() + if (!hasPrev()) return + setDirection(PageDirection.PREV) + readView.setStartPoint(0f, viewHeight.toFloat(), false) + onAnimStart(animationSpeed) + } + + override fun onDestroy() { + super.onDestroy() + prevRecorder.recycle() + curRecorder.recycle() + nextRecorder.recycle() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt new file mode 100755 index 000000000..aaaa072d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt @@ -0,0 +1,28 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.graphics.Canvas +import io.legado.app.ui.book.read.page.ReadView + +class NoAnimPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { + + override fun onAnimStart(animationSpeed: Int) { + if (!isCancel) { + readView.fillPage(mDirection) + } + stopScroll() + } + + override fun setBitmap() { + // nothing + } + + override fun onDraw(canvas: Canvas) { + // nothing + } + + override fun onAnimStop() { + // nothing + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt new file mode 100755 index 000000000..374622a1b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -0,0 +1,208 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.content.Context +import android.graphics.Canvas +import android.view.MotionEvent +import android.view.animation.LinearInterpolator +import android.widget.Scroller +import androidx.annotation.CallSuper +import com.google.android.material.snackbar.Snackbar +import io.legado.app.R +import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import kotlin.math.abs + +abstract class PageDelegate(protected val readView: ReadView) { + + protected val context: Context = readView.context + + //起始点 + protected val startX: Float get() = readView.startX + protected val startY: Float get() = readView.startY + + //上一个触碰点 + protected val lastX: Float get() = readView.lastX + protected val lastY: Float get() = readView.lastY + + //触碰点 + protected val touchX: Float get() = readView.touchX + protected val touchY: Float get() = readView.touchY + + protected val nextPage: PageView get() = readView.nextPage + protected val curPage: PageView get() = readView.curPage + protected val prevPage: PageView get() = readView.prevPage + + protected var viewWidth: Int = readView.width + protected var viewHeight: Int = readView.height + + protected val scroller: Scroller by lazy { + Scroller(readView.context, LinearInterpolator()) + } + + private val snackBar: Snackbar by lazy { + Snackbar.make(readView, "", Snackbar.LENGTH_SHORT) + } + + var isMoved = false + var noNext = true + + //移动方向 + var mDirection = PageDirection.NONE + var isCancel = false + var isRunning = false + var isStarted = false + + private var selectedOnDown = false + + init { + curPage.resetPageOffset() + } + + open fun fling( + startX: Int, startY: Int, velocityX: Int, velocityY: Int, + minX: Int, maxX: Int, minY: Int, maxY: Int + ) { + scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) + isRunning = true + isStarted = true + readView.invalidate() + } + + protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int, animationSpeed: Int) { + val duration = if (dx != 0) { + (animationSpeed * abs(dx)) / viewWidth + } else { + (animationSpeed * abs(dy)) / viewHeight + } + scroller.startScroll(startX, startY, dx, dy, duration) + isRunning = true + isStarted = true + readView.invalidate() + } + + protected fun stopScroll() { + isStarted = false + readView.post { + isMoved = false + isRunning = false + readView.invalidate() + } + } + + @CallSuper + open fun setViewSize(width: Int, height: Int) { + viewWidth = width + viewHeight = height + } + + open fun computeScroll() { + if (scroller.computeScrollOffset()) { + readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) + } else if (isStarted) { + onAnimStop() + stopScroll() + } + } + + open fun onScroll() = Unit + + abstract fun abortAnim() + + abstract fun onAnimStart(animationSpeed: Int) //scroller start + + abstract fun onDraw(canvas: Canvas) //绘制 + + abstract fun onAnimStop() //scroller finish + + abstract fun nextPageByAnim(animationSpeed: Int) + + abstract fun prevPageByAnim(animationSpeed: Int) + + open fun keyTurnPage(direction: PageDirection) { + if (isRunning) return + when (direction) { + PageDirection.NEXT -> nextPageByAnim(100) + PageDirection.PREV -> prevPageByAnim(100) + else -> return + } + } + + @CallSuper + open fun setDirection(direction: PageDirection) { + mDirection = direction + } + + /** + * 触摸事件处理 + */ + abstract fun onTouch(event: MotionEvent) + + /** + * 按下 + */ + fun onDown() { + //是否移动 + isMoved = false + //是否存在下一章 + noNext = false + //是否正在执行动画 + isRunning = false + //取消 + isCancel = false + //是下一章还是前一章 + setDirection(PageDirection.NONE) + } + + /** + * 判断是否有上一页 + */ + fun hasPrev(): Boolean { + val hasPrev = readView.pageFactory.hasPrev() + if (!hasPrev) { + if (!snackBar.isShown) { + snackBar.setText(R.string.no_prev_page) + snackBar.show() + } + } + return hasPrev + } + + /** + * 判断是否有下一页 + */ + fun hasNext(): Boolean { + val hasNext = readView.pageFactory.hasNext() + if (!hasNext) { + readView.callBack.autoPageStop() + if (!snackBar.isShown) { + snackBar.setText(R.string.no_next_page) + snackBar.show() + } + } + return hasNext + } + + fun dismissSnackBar() { + // 判断snackBar是否显示,并关闭 + if (snackBar.isShown) { + snackBar.dismiss() + } + } + + fun postInvalidate() { + if (isStarted && isRunning && this is HorizontalPageDelegate) { + readView.post { + if (isStarted && isRunning) { + setBitmap() + readView.invalidate() + } + } + } + } + + open fun onDestroy() { + // run on destroy + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt new file mode 100755 index 000000000..bec255657 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt @@ -0,0 +1,186 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.graphics.Canvas +import android.view.MotionEvent +import android.view.VelocityTracker +import io.legado.app.data.entities.Book +import io.legado.app.help.book.isImage +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.provider.ChapterProvider + +class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) { + + // 滑动追踪的时间 + private val velocityDuration = 1000 + + //速度追踪器 + private val mVelocity: VelocityTracker = VelocityTracker.obtain() + private val slopSquare get() = readView.pageSlopSquare2 + + var noAnim: Boolean = false + + override fun onAnimStart(animationSpeed: Int) { + readView.onScrollAnimStart() + //惯性滚动 + fling( + 0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(), + 0, 0, -10 * viewHeight, 10 * viewHeight + ) + } + + override fun onAnimStop() { + readView.onScrollAnimStop() + } + + override fun onTouch(event: MotionEvent) { + //在多点触控时,事件不走ACTION_DOWN分支而产生的特殊事件处理 + if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN) { + //当多个手指同时按下的情况,将最后一个按下的手指的坐标设置为起始坐标,所以只有最后一个手指的滑动事件被处理 + readView.setStartPoint( + event.getX(event.pointerCount - 1), + event.getY(event.pointerCount - 1), + false + ) + } else if (event.actionMasked == MotionEvent.ACTION_POINTER_UP) { + //当多个手指同时按下的情况,当抬起一个手指时,起始坐标恢复为第一次按下的手指的坐标 + readView.setStartPoint(event.x, event.y, false) + return + } + when (event.action) { + MotionEvent.ACTION_DOWN -> { + abortAnim() + mVelocity.clear() + } + + MotionEvent.ACTION_MOVE -> { + onScroll(event) + } + + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + onAnimStart(readView.defaultAnimationSpeed) + } + } + } + + override fun onScroll() { + curPage.scroll((touchY - lastY).toInt()) + } + + override fun onDraw(canvas: Canvas) { + // nothing + } + + private fun onScroll(event: MotionEvent) { + mVelocity.addMovement(event) + mVelocity.computeCurrentVelocity(velocityDuration) + //取最后添加(即最新的)一个触摸点来计算滚动位置 + //多点触控时即最后按下的手指产生的事件点 + val pointX = event.getX(event.pointerCount - 1) + val pointY = event.getY(event.pointerCount - 1) + if (isMoved || readView.isLongScreenShot()) { + readView.setTouchPoint(pointX, pointY, false) + } + if (!isMoved) { + val deltaX = (pointX - startX).toInt() + val deltaY = (pointY - startY).toInt() + val distance = deltaX * deltaX + deltaY * deltaY + isMoved = distance > slopSquare + if (isMoved) { + readView.setStartPoint(event.x, event.y, false) + } + } + if (isMoved) { + isRunning = true + } + } + + override fun computeScroll() { + if (scroller.computeScrollOffset()) { + readView.setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat(), false) + } else if (isStarted) { + onAnimStop() + stopScroll() + } + } + + override fun onDestroy() { + super.onDestroy() + mVelocity.recycle() + } + + override fun abortAnim() { + readView.onScrollAnimStop() + isStarted = false + isMoved = false + isRunning = false + if (!scroller.isFinished) { + readView.isAbortAnim = true + scroller.abortAnimation() + } else { + readView.isAbortAnim = false + } + } + + override fun nextPageByAnim(animationSpeed: Int) { + if (readView.isAbortAnim) { + readView.isAbortAnim = false + return + } + if (noAnim) { + curPage.scroll(calcNextPageOffset()) + return + } + readView.setStartPoint(0f, 0f, false) + startScroll(0, 0, 0, calcNextPageOffset(), animationSpeed) + } + + override fun prevPageByAnim(animationSpeed: Int) { + if (readView.isAbortAnim) { + readView.isAbortAnim = false + return + } + if (noAnim) { + curPage.scroll(calcPrevPageOffset()) + return + } + readView.setStartPoint(0f, 0f, false) + startScroll(0, 0, 0, calcPrevPageOffset(), animationSpeed) + } + + /** + * 计算点击翻页保留一行的滚动距离 + * 图片页使用可视高度作为滚动距离 + */ + private fun calcNextPageOffset(): Int { + val visibleHeight = ChapterProvider.visibleHeight + val book = ReadBook.book + if (book == null || book.isImage) { + return -visibleHeight + } + val visiblePage = readView.getCurVisiblePage() + val isTextStyle = book.getImageStyle().equals(Book.imgStyleText, true) + if (!isTextStyle && visiblePage.hasImageOrEmpty()) { + return -visibleHeight + } + val lastLineTop = visiblePage.lines.last().lineTop.toInt() + val offset = lastLineTop - ChapterProvider.paddingTop + return -offset + } + + private fun calcPrevPageOffset(): Int { + val visibleHeight = ChapterProvider.visibleHeight + val book = ReadBook.book + if (book == null || book.isImage) { + return visibleHeight + } + val visiblePage = readView.getCurVisiblePage() + val isTextStyle = book.getImageStyle().equals(Book.imgStyleText, true) + if (!isTextStyle && visiblePage.hasImageOrEmpty()) { + return visibleHeight + } + val firstLineBottom = visiblePage.lines.first().lineBottom.toInt() + val offset = visibleHeight - (firstLineBottom - ChapterProvider.paddingTop) + return offset + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt new file mode 100755 index 000000000..ebadb8dad --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt @@ -0,0 +1,613 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PointF +import android.graphics.Region +import android.graphics.drawable.GradientDrawable +import android.os.Build +import android.view.MotionEvent +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection +import io.legado.app.utils.screenshot +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.hypot +import kotlin.math.min +import kotlin.math.sin + +@Suppress("DEPRECATION") +class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { + //不让x,y为0,否则在点计算时会有问题 + private var mTouchX = 0.1f + private var mTouchY = 0.1f + + // 拖拽点对应的页脚 + private var mCornerX = 1 + private var mCornerY = 1 + private val mPath0: Path = Path() + private val mPath1: Path = Path() + + // 贝塞尔曲线起始点 + private val mBezierStart1 = PointF() + + // 贝塞尔曲线控制点 + private val mBezierControl1 = PointF() + + // 贝塞尔曲线顶点 + private val mBezierVertex1 = PointF() + + // 贝塞尔曲线结束点 + private var mBezierEnd1 = PointF() + + // 另一条贝塞尔曲线 + // 贝塞尔曲线起始点 + private val mBezierStart2 = PointF() + + // 贝塞尔曲线控制点 + private val mBezierControl2 = PointF() + + // 贝塞尔曲线顶点 + private val mBezierVertex2 = PointF() + + // 贝塞尔曲线结束点 + private var mBezierEnd2 = PointF() + + private var mMiddleX = 0f + private var mMiddleY = 0f + private var mDegrees = 0f + private var mTouchToCornerDis = 0f + private var mColorMatrixFilter = ColorMatrixColorFilter( + ColorMatrix( + floatArrayOf( + 1f, 0f, 0f, 0f, 0f, + 0f, 1f, 0f, 0f, 0f, + 0f, 0f, 1f, 0f, 0f, + 0f, 0f, 0f, 1f, 0f + ) + ) + ) + private val mMatrix: Matrix = Matrix() + private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f) + + // 是否属于右上左下 + private var mIsRtOrLb = false + private var mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat() + + // 背面颜色组 + private var mBackShadowColors: IntArray + + // 前面颜色组 + private var mFrontShadowColors: IntArray + + // 有阴影的GradientDrawable + private var mBackShadowDrawableLR: GradientDrawable + private var mBackShadowDrawableRL: GradientDrawable + private var mFolderShadowDrawableLR: GradientDrawable + private var mFolderShadowDrawableRL: GradientDrawable + + private var mFrontShadowDrawableHBT: GradientDrawable + private var mFrontShadowDrawableHTB: GradientDrawable + private var mFrontShadowDrawableVLR: GradientDrawable + private var mFrontShadowDrawableVRL: GradientDrawable + + private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL } + + private var curBitmap: Bitmap? = null + private var prevBitmap: Bitmap? = null + private var nextBitmap: Bitmap? = null + private var canvas: Canvas = Canvas() + + init { + //设置颜色数组 + val color = intArrayOf(0x333333, -0x4fcccccd) + mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color) + mFolderShadowDrawableRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFolderShadowDrawableLR = GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, color) + mFolderShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) + mBackShadowDrawableRL = + GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors) + mBackShadowDrawableRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mBackShadowDrawableLR = + GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors) + mBackShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) + mFrontShadowDrawableVLR = + GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors) + mFrontShadowDrawableVLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableVRL = + GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors) + mFrontShadowDrawableVRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableHTB = + GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors) + mFrontShadowDrawableHTB.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableHBT = + GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors) + mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT + } + + override fun setBitmap() { + when (mDirection) { + PageDirection.PREV -> { + prevBitmap = prevPage.screenshot(prevBitmap, canvas) + curBitmap = curPage.screenshot(curBitmap, canvas) + } + + PageDirection.NEXT -> { + nextBitmap = nextPage.screenshot(nextBitmap, canvas) + curBitmap = curPage.screenshot(curBitmap, canvas) + } + + else -> Unit + } + } + + override fun setViewSize(width: Int, height: Int) { + super.setViewSize(width, height) + mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat() + } + + override fun onTouch(event: MotionEvent) { + super.onTouch(event) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + calcCornerXY(event.x, event.y) + } + + MotionEvent.ACTION_MOVE -> { + if ((startY > viewHeight / 3 && startY < viewHeight * 2 / 3) + || mDirection == PageDirection.PREV + ) { + readView.touchY = viewHeight.toFloat() + } + + if (startY > viewHeight / 3 && startY < viewHeight / 2 + && mDirection == PageDirection.NEXT + ) { + readView.touchY = 1f + } + } + } + } + + override fun setDirection(direction: PageDirection) { + super.setDirection(direction) + when (direction) { + PageDirection.PREV -> + //上一页滑动不出现对角 + if (startX > viewWidth / 2) { + calcCornerXY(startX, viewHeight.toFloat()) + } else { + calcCornerXY(viewWidth - startX, viewHeight.toFloat()) + } + + PageDirection.NEXT -> + if (viewWidth / 2 > startX) { + calcCornerXY(viewWidth - startX, startY) + } + + else -> Unit + } + } + + override fun onAnimStart(animationSpeed: Int) { + var dx: Float + val dy: Float + // dy 垂直方向滑动的距离,负值会使滚动向上滚动 + if (isCancel) { + dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) { + (viewWidth - touchX) + } else { + -touchX + } + if (mDirection != PageDirection.NEXT) { + dx = -(viewWidth + touchX) + } + dy = if (mCornerY > 0) { + (viewHeight - touchY) + } else { + -touchY // 防止mTouchY最终变为0 + } + } else { + dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) { + -(viewWidth + touchX) + } else { + viewWidth - touchX + } + dy = if (mCornerY > 0) { + (viewHeight - touchY) + } else { + (1 - touchY) // 防止mTouchY最终变为0 + } + } + startScroll(touchX.toInt(), touchY.toInt(), dx.toInt(), dy.toInt(), animationSpeed) + } + + override fun onAnimStop() { + if (!isCancel) { + readView.fillPage(mDirection) + } + } + + override fun onDraw(canvas: Canvas) { + if (!isRunning) return + when (mDirection) { + PageDirection.NEXT -> { + calcPoints() + drawCurrentPageArea(canvas, curBitmap) + drawNextPageAreaAndShadow(canvas, nextBitmap) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, curBitmap) + } + + PageDirection.PREV -> { + calcPoints() + drawCurrentPageArea(canvas, prevBitmap) + drawNextPageAreaAndShadow(canvas, curBitmap) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, prevBitmap) + } + + else -> return + } + } + + /** + * 绘制翻起页背面 + */ + private fun drawCurrentBackArea( + canvas: Canvas, + bitmap: Bitmap? + ) { + bitmap ?: return + val i = ((mBezierStart1.x + mBezierControl1.x) / 2).toInt() + val f1 = abs(i - mBezierControl1.x) + val i1 = ((mBezierStart2.y + mBezierControl2.y) / 2).toInt() + val f2 = abs(i1 - mBezierControl2.y) + val f3 = min(f1, f2) + mPath1.reset() + mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y) + mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) + mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y) + mPath1.lineTo(mTouchX, mTouchY) + mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y) + mPath1.close() + val mFolderShadowDrawable: GradientDrawable + val left: Int + val right: Int + if (mIsRtOrLb) { + left = (mBezierStart1.x - 1).toInt() + right = (mBezierStart1.x + f3 + 1).toInt() + mFolderShadowDrawable = mFolderShadowDrawableLR + } else { + left = (mBezierStart1.x - f3 - 1).toInt() + right = (mBezierStart1.x + 1).toInt() + mFolderShadowDrawable = mFolderShadowDrawableRL + } + canvas.save() + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) + } else { + canvas.clipPath(mPath1, Region.Op.INTERSECT) + } + + mPaint.colorFilter = mColorMatrixFilter + val dis = hypot( + mCornerX - mBezierControl1.x.toDouble(), + mBezierControl2.y - mCornerY.toDouble() + ).toFloat() + val f8 = (mCornerX - mBezierControl1.x) / dis + val f9 = (mBezierControl2.y - mCornerY) / dis + mMatrixArray[0] = 1 - 2 * f9 * f9 + mMatrixArray[1] = 2 * f8 * f9 + mMatrixArray[3] = mMatrixArray[1] + mMatrixArray[4] = 1 - 2 * f8 * f8 + mMatrix.reset() + mMatrix.setValues(mMatrixArray) + mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y) + mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y) + canvas.drawColor(ReadBookConfig.bgMeanColor) + canvas.drawBitmap(bitmap, mMatrix, mPaint) + mPaint.colorFilter = null + canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) + mFolderShadowDrawable.setBounds( + left, mBezierStart1.y.toInt(), + right, (mBezierStart1.y + mMaxLength).toInt() + ) + mFolderShadowDrawable.draw(canvas) + canvas.restore() + } + + /** + * 绘制翻起页的阴影 + */ + private fun drawCurrentPageShadow(canvas: Canvas) { + val degree: Double = if (mIsRtOrLb) { + Math.PI / 4 - atan2(mBezierControl1.y - mTouchY, mTouchX - mBezierControl1.x) + } else { + Math.PI / 4 - atan2(mTouchY - mBezierControl1.y, mTouchX - mBezierControl1.x) + } + // 翻起页阴影顶点与touch点的距离 + val d1 = 25.toFloat() * 1.414 * cos(degree) + val d2 = 25.toFloat() * 1.414 * sin(degree) + val x = (mTouchX + d1).toFloat() + val y: Float = if (mIsRtOrLb) { + (mTouchY + d2).toFloat() + } else { + (mTouchY - d2).toFloat() + } + mPath1.reset() + mPath1.moveTo(x, y) + mPath1.lineTo(mTouchX, mTouchY) + mPath1.lineTo(mBezierControl1.x, mBezierControl1.y) + mPath1.lineTo(mBezierStart1.x, mBezierStart1.y) + mPath1.close() + canvas.save() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) + } + canvas.clipPath(mPath1, Region.Op.INTERSECT) + + var leftX: Int + var rightX: Int + var mCurrentPageShadow: GradientDrawable + if (mIsRtOrLb) { + leftX = mBezierControl1.x.toInt() + rightX = (mBezierControl1.x + 25).toInt() + mCurrentPageShadow = mFrontShadowDrawableVLR + } else { + leftX = (mBezierControl1.x - 25).toInt() + rightX = (mBezierControl1.x + 1).toInt() + mCurrentPageShadow = mFrontShadowDrawableVRL + } + var rotateDegrees = Math.toDegrees( + atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble() + ).toFloat() + canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) + mCurrentPageShadow.setBounds( + leftX, (mBezierControl1.y - mMaxLength).toInt(), + rightX, mBezierControl1.y.toInt() + ) + mCurrentPageShadow.draw(canvas) + canvas.restore() + + mPath1.reset() + mPath1.moveTo(x, y) + mPath1.lineTo(mTouchX, mTouchY) + mPath1.lineTo(mBezierControl2.x, mBezierControl2.y) + mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) + mPath1.close() + canvas.save() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) + } + canvas.clipPath(mPath1) + + if (mIsRtOrLb) { + leftX = mBezierControl2.y.toInt() + rightX = (mBezierControl2.y + 25).toInt() + mCurrentPageShadow = mFrontShadowDrawableHTB + } else { + leftX = (mBezierControl2.y - 25).toInt() + rightX = (mBezierControl2.y + 1).toInt() + mCurrentPageShadow = mFrontShadowDrawableHBT + } + rotateDegrees = Math.toDegrees( + atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble() + ).toFloat() + canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) + val temp = + if (mBezierControl2.y < 0) (mBezierControl2.y - viewHeight).toDouble() + else mBezierControl2.y.toDouble() + val hmg = hypot(mBezierControl2.x.toDouble(), temp) + if (hmg > mMaxLength) + mCurrentPageShadow.setBounds( + (mBezierControl2.x - 25 - hmg).toInt(), leftX, + (mBezierControl2.x + mMaxLength - hmg).toInt(), rightX + ) + else + mCurrentPageShadow.setBounds( + (mBezierControl2.x - mMaxLength).toInt(), leftX, + mBezierControl2.x.toInt(), rightX + ) + mCurrentPageShadow.draw(canvas) + canvas.restore() + } + + // + private fun drawNextPageAreaAndShadow( + canvas: Canvas, + bitmap: Bitmap? + ) { + bitmap ?: return + mPath1.reset() + mPath1.moveTo(mBezierStart1.x, mBezierStart1.y) + mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) + mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y) + mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) + mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) + mPath1.close() + mDegrees = Math.toDegrees( + atan2( + (mBezierControl1.x - mCornerX).toDouble(), + mBezierControl2.y - mCornerY.toDouble() + ) + ).toFloat() + val leftX: Int + val rightX: Int + val mBackShadowDrawable: GradientDrawable + if (mIsRtOrLb) { //左下及右上 + leftX = mBezierStart1.x.toInt() + rightX = (mBezierStart1.x + mTouchToCornerDis / 4).toInt() + mBackShadowDrawable = mBackShadowDrawableLR + } else { + leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt() + rightX = mBezierStart1.x.toInt() + mBackShadowDrawable = mBackShadowDrawableRL + } + canvas.save() + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) + } else { + canvas.clipPath(mPath1, Region.Op.INTERSECT) + } + canvas.drawBitmap(bitmap, 0f, 0f, null) + canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) + mBackShadowDrawable.setBounds( + leftX, mBezierStart1.y.toInt(), + rightX, (mMaxLength + mBezierStart1.y).toInt() + ) //左上及右下角的xy坐标值,构成一个矩形 + mBackShadowDrawable.draw(canvas) + canvas.restore() + } + + // + private fun drawCurrentPageArea( + canvas: Canvas, + bitmap: Bitmap? + ) { + bitmap ?: return + mPath0.reset() + mPath0.moveTo(mBezierStart1.x, mBezierStart1.y) + mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, mBezierEnd1.y) + mPath0.lineTo(mTouchX, mTouchY) + mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y) + mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y) + mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) + mPath0.close() + + canvas.save() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) + } + canvas.drawBitmap(bitmap, 0f, 0f, null) + canvas.restore() + } + + /** + * 计算拖拽点对应的拖拽脚 + */ + private fun calcCornerXY(x: Float, y: Float) { + mCornerX = if (x <= viewWidth / 2) 0 else viewWidth + mCornerY = if (y <= viewHeight / 2) 0 else viewHeight + mIsRtOrLb = (mCornerX == 0 && mCornerY == viewHeight) + || (mCornerY == 0 && mCornerX == viewWidth) + } + + private fun calcPoints() { + mTouchX = touchX + mTouchY = touchY + + mMiddleX = (mTouchX + mCornerX) / 2 + mMiddleY = (mTouchY + mCornerY) / 2 + mBezierControl1.x = + mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) + mBezierControl1.y = mCornerY.toFloat() + mBezierControl2.x = mCornerX.toFloat() + + val f4 = mCornerY - mMiddleY + if (f4 == 0f) { + mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f + + } else { + mBezierControl2.y = + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) + } + mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 + mBezierStart1.y = mCornerY.toFloat() + + // 固定左边上下两个点 + if (mTouchX > 0 && mTouchX < viewWidth) { + if (mBezierStart1.x < 0 || mBezierStart1.x > viewWidth) { + if (mBezierStart1.x < 0) + mBezierStart1.x = viewWidth - mBezierStart1.x + + val f1 = abs(mCornerX - mTouchX) + val f2 = viewWidth * f1 / mBezierStart1.x + mTouchX = abs(mCornerX - f2) + + val f3 = abs(mCornerX - mTouchX) * abs(mCornerY - mTouchY) / f1 + mTouchY = abs(mCornerY - f3) + + mMiddleX = (mTouchX + mCornerX) / 2 + mMiddleY = (mTouchY + mCornerY) / 2 + + mBezierControl1.x = + mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) + mBezierControl1.y = mCornerY.toFloat() + + mBezierControl2.x = mCornerX.toFloat() + + val f5 = mCornerY - mMiddleY + if (f5 == 0f) { + mBezierControl2.y = + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f + } else { + mBezierControl2.y = + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) + } + + mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 + } + } + mBezierStart2.x = mCornerX.toFloat() + mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2 + + mTouchToCornerDis = hypot( + (mTouchX - mCornerX).toDouble(), + (mTouchY - mCornerY).toDouble() + ).toFloat() + + mBezierEnd1 = getCross( + PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1, + mBezierStart2 + ) + mBezierEnd2 = getCross( + PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1, + mBezierStart2 + ) + + mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 + mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 + mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 + mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4 + } + + /** + * 求解直线P1P2和直线P3P4的交点坐标 + */ + private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF { + val crossP = PointF() + // 二元函数通式: y=ax+b + val a1 = (P2.y - P1.y) / (P2.x - P1.x) + val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x) + val a2 = (P4.y - P3.y) / (P4.x - P3.x) + val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x) + crossP.x = (b2 - b1) / (a1 - a2) + crossP.y = a1 * crossP.x + b1 + return crossP + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt new file mode 100755 index 000000000..33c6480cb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt @@ -0,0 +1,64 @@ +package io.legado.app.ui.book.read.page.delegate + +import android.graphics.Canvas +import androidx.core.graphics.withTranslation +import io.legado.app.ui.book.read.page.ReadView +import io.legado.app.ui.book.read.page.entities.PageDirection + +class SlidePageDelegate(readView: ReadView) : HorizontalPageDelegate(readView) { + + override fun onAnimStart(animationSpeed: Int) { + val distanceX: Float + when (mDirection) { + PageDirection.NEXT -> distanceX = + if (isCancel) { + var dis = viewWidth - startX + touchX + if (dis > viewWidth) { + dis = viewWidth.toFloat() + } + viewWidth - dis + } else { + -(touchX + (viewWidth - startX)) + } + + else -> distanceX = + if (isCancel) { + -(touchX - startX) + } else { + viewWidth - (touchX - startX) + } + } + startScroll(touchX.toInt(), 0, distanceX.toInt(), 0, animationSpeed) + } + + override fun onDraw(canvas: Canvas) { + val offsetX = touchX - startX + + if ((mDirection == PageDirection.NEXT && offsetX > 0) + || (mDirection == PageDirection.PREV && offsetX < 0) + ) return + val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth + if (!isRunning) return + if (mDirection == PageDirection.PREV) { + canvas.withTranslation(distanceX + viewWidth) { + curRecorder.draw(this) + } + canvas.withTranslation(distanceX) { + prevRecorder.draw(this) + } + } else if (mDirection == PageDirection.NEXT) { + canvas.withTranslation(distanceX) { + nextRecorder.draw(this) + } + canvas.withTranslation(distanceX - viewWidth) { + curRecorder.draw(this) + } + } + } + + override fun onAnimStop() { + if (!isCancel) { + readView.fillPage(mDirection) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt new file mode 100755 index 000000000..713c6bd67 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/PageDirection.kt @@ -0,0 +1,5 @@ +package io.legado.app.ui.book.read.page.entities + +enum class PageDirection { + NONE, PREV, NEXT +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt new file mode 100755 index 000000000..b48c37850 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt @@ -0,0 +1,321 @@ +package io.legado.app.ui.book.read.page.entities + + +import androidx.annotation.Keep +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.help.book.BookContent +import io.legado.app.ui.book.read.page.provider.LayoutProgressListener +import io.legado.app.ui.book.read.page.provider.TextChapterLayout +import io.legado.app.utils.fastBinarySearchBy +import kotlinx.coroutines.CoroutineScope +import kotlin.math.abs +import kotlin.math.min + +/** + * 章节信息 + */ +@Keep +@Suppress("unused") +data class TextChapter( + val chapter: BookChapter, + val position: Int, + val title: String, + val chaptersSize: Int, + val sameTitleRemoved: Boolean, + val isVip: Boolean, + val isPay: Boolean, + //起效的替换规则 + val effectiveReplaceRules: List? +) : LayoutProgressListener { + + private val textPages = arrayListOf() + val pages: List get() = textPages + + private var layout: TextChapterLayout? = null + + val layoutChannel get() = layout!!.channel + + fun getPage(index: Int): TextPage? { + return pages.getOrNull(index) + } + + fun getPageByReadPos(readPos: Int): TextPage? { + return getPage(getPageIndexByCharIndex(readPos)) + } + + val lastPage: TextPage? get() = pages.lastOrNull() + + val lastIndex: Int get() = pages.lastIndex + + val lastReadLength: Int get() = getReadLength(lastIndex) + + val pageSize: Int get() = pages.size + + var listener: LayoutProgressListener? = null + + var isCompleted = false + + val paragraphs by lazy { + paragraphsInternal + } + + val pageParagraphs by lazy { + pageParagraphsInternal + } + + val paragraphsInternal: ArrayList + get() { + val paragraphs = arrayListOf() + for (i in pages.indices) { + val lines = pages[i].lines + for (a in lines.indices) { + val line = lines[a] + if (line.paragraphNum <= 0) continue + if (paragraphs.lastIndex < line.paragraphNum - 1) { + paragraphs.add(TextParagraph(line.paragraphNum)) + } + paragraphs[line.paragraphNum - 1].textLines.add(line) + } + } + return paragraphs + } + + val pageParagraphsInternal: List + get() { + val paragraphs = arrayListOf() + for (i in pages.indices) { + paragraphs.addAll(pages[i].paragraphs) + } + for (i in paragraphs.indices) { + paragraphs[i].num = i + 1 + } + return paragraphs + } + + /** + * @param index 页数 + * @return 是否是最后一页 + */ + fun isLastIndex(index: Int): Boolean { + return isCompleted && index >= pages.size - 1 + } + + fun isLastIndexCurrent(index: Int): Boolean { + return index >= pages.size - 1 + } + + /** + * @param pageIndex 页数 + * @return 已读长度 + */ + fun getReadLength(pageIndex: Int): Int { + if (pageIndex < 0) return 0 + return pages[min(pageIndex, lastIndex)].chapterPosition + /* + var length = 0 + val maxIndex = min(pageIndex, pages.size) + for (index in 0 until maxIndex) { + length += pages[index].charSize + } + return length + */ + } + + /** + * @param length 当前页面文字在章节中的位置 + * @return 下一页位置,如果没有下一页返回-1 + */ + fun getNextPageLength(length: Int): Int { + val pageIndex = getPageIndexByCharIndex(length) + if (pageIndex + 1 >= pageSize) { + return -1 + } + return getReadLength(pageIndex + 1) + } + + /** + * @param length 当前页面文字在章节中的位置 + * @return 上一页位置,如果没有上一页返回-1 + */ + fun getPrevPageLength(length: Int): Int { + val pageIndex = getPageIndexByCharIndex(length) + if (pageIndex - 1 < 0) { + return -1 + } + return getReadLength(pageIndex - 1) + } + + /** + * 获取内容 + */ + fun getContent(): String { + val stringBuilder = StringBuilder() + pages.forEach { + stringBuilder.append(it.text) + } + return stringBuilder.toString() + } + + /** + * @return 获取未读文字 + */ + fun getUnRead(pageIndex: Int): String { + val stringBuilder = StringBuilder() + if (pages.isNotEmpty()) { + for (index in pageIndex..pages.lastIndex) { + stringBuilder.append(pages[index].text) + } + } + return stringBuilder.toString() + } + + /** + * @return 需要朗读的文本列表 + * @param pageIndex 起始页 + * @param pageSplit 是否分页 + * @param startPos 从当前页什么地方开始朗读 + */ + fun getNeedReadAloud( + pageIndex: Int, + pageSplit: Boolean, + startPos: Int, + pageEndIndex: Int = pages.lastIndex + ): String { + val stringBuilder = StringBuilder() + if (pages.isNotEmpty()) { + for (index in pageIndex..min(pageEndIndex, pages.lastIndex)) { + stringBuilder.append(pages[index].text) + if (pageSplit && !stringBuilder.endsWith("\n")) { + stringBuilder.append("\n") + } + } + } + return stringBuilder.substring(startPos).toString() + } + + fun getParagraphNum( + position: Int, + pageSplit: Boolean, + ): Int { + val paragraphs = getParagraphs(pageSplit) + paragraphs.forEach { paragraph -> + if (position in paragraph.chapterIndices) { + return paragraph.num + } + } + return -1 + } + + fun getParagraphs(pageSplit: Boolean): List { + return if (pageSplit) { + if (isCompleted) pageParagraphs else pageParagraphsInternal + } else { + if (isCompleted) paragraphs else paragraphsInternal + } + } + + fun getLastParagraphPosition(): Int { + return pageParagraphs.last().chapterPosition + } + + /** + * @return 根据索引位置获取所在页 + */ + fun getPageIndexByCharIndex(charIndex: Int): Int { + val pageSize = pages.size + if (pageSize == 0) { + return -1 + } + val bIndex = pages.fastBinarySearchBy(charIndex, 0, pageSize) { + it.chapterPosition + } + val index = abs(bIndex + 1) - 1 + // 判断是否已经排版到 charIndex ,没有则返回 -1 + if (!isCompleted && index == pageSize - 1) { + val page = pages[index] + val pageEndPos = page.chapterPosition + page.charSize + if (charIndex > pageEndPos) { + return -1 + } + } + return index + /* + var length = 0 + for (i in pages.indices) { + val page = pages[i] + length += page.charSize + if (length > charIndex) { + return page.index + } + } + return pages.lastIndex + */ + } + + fun clearSearchResult() { + for (i in pages.indices) { + val page = pages[i] + page.searchResult.forEach { + it.selected = false + it.isSearchResult = false + } + page.searchResult.clear() + } + } + + fun createLayout(scope: CoroutineScope, book: Book, bookContent: BookContent) { + if (layout != null) { + throw IllegalStateException("已经排版过了") + } + layout = TextChapterLayout( + scope, + this, + textPages, + book, + bookContent, + ) + } + + fun setProgressListener(l: LayoutProgressListener?) { + if (isCompleted) { + // no op + } else if (layout?.exception != null) { + l?.onLayoutException(layout?.exception!!) + } else { + listener = l + } + } + + override fun onLayoutPageCompleted(index: Int, page: TextPage) { + listener?.onLayoutPageCompleted(index, page) + } + + override fun onLayoutCompleted() { + isCompleted = true + listener?.onLayoutCompleted() + listener = null + } + + override fun onLayoutException(e: Throwable) { + listener?.onLayoutException(e) + listener = null + } + + fun cancelLayout() { + layout?.cancel() + listener = null + } + + companion object { + val emptyTextChapter = TextChapter( + BookChapter(), -1, "emptyTextChapter", -1, + sameTitleRemoved = false, + isVip = false, + isPay = false, + null + ).apply { isCompleted = true } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt new file mode 100755 index 000000000..d981c33ce --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -0,0 +1,274 @@ +package io.legado.app.ui.book.read.page.entities + +import android.annotation.SuppressLint +import android.graphics.Canvas +import android.graphics.Paint.FontMetrics +import android.os.Build +import androidx.annotation.Keep +import io.legado.app.help.PaintPool +import io.legado.app.help.book.isImage +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextPage.Companion.emptyTextPage +import io.legado.app.ui.book.read.page.entities.column.BaseColumn +import io.legado.app.ui.book.read.page.entities.column.TextColumn +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw +import io.legado.app.utils.dpToPx + +/** + * 行信息 + */ +@Keep +@Suppress("unused", "MemberVisibilityCanBePrivate") +data class TextLine( + var text: String = "", + private val textColumns: ArrayList = arrayListOf(), + var lineTop: Float = 0f, + var lineBase: Float = 0f, + var lineBottom: Float = 0f, + var indentWidth: Float = 0f, + var paragraphNum: Int = 0, + var chapterPosition: Int = 0, + var pagePosition: Int = 0, + val isTitle: Boolean = false, + var isParagraphEnd: Boolean = false, + var isImage: Boolean = false, + var startX: Float = 0f, + var indentSize: Int = 0, + var extraLetterSpacing: Float = 0f, + var extraLetterSpacingOffsetX: Float = 0f, + var wordSpacing: Float = 0f, + var exceed: Boolean = false, + var onlyTextColumn: Boolean = true, +) { + + val columns: List get() = textColumns + val charSize: Int get() = text.length + val lineStart: Float get() = textColumns.firstOrNull()?.start ?: 0f + val lineEnd: Float get() = textColumns.lastOrNull()?.end ?: 0f + val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize + val height: Float inline get() = lineBottom - lineTop + val canvasRecorder = CanvasRecorderFactory.create() + var searchResultColumnCount = 0 + var isReadAloud: Boolean = false + set(value) { + if (field != value) { + invalidate() + } + if (value) { + textPage.hasReadAloudSpan = true + } + field = value + } + var textPage: TextPage = emptyTextPage + var isLeftLine = true + + fun addColumn(column: BaseColumn) { + if (column !is TextColumn) { + onlyTextColumn = false + } + column.textLine = this + textColumns.add(column) + } + + fun getColumn(index: Int): BaseColumn { + return textColumns.getOrElse(index) { + textColumns.last() + } + } + + fun getColumnReverseAt(index: Int, offset: Int = 0): BaseColumn { + return textColumns[textColumns.lastIndex - offset - index] + } + + fun getColumnsCount(): Int { + return textColumns.size + } + + fun upTopBottom(durY: Float, textHeight: Float, fontMetrics: FontMetrics) { + lineTop = ChapterProvider.paddingTop + durY + lineBottom = lineTop + textHeight + lineBase = lineBottom - fontMetrics.descent + } + + fun isTouch(x: Float, y: Float, relativeOffset: Float): Boolean { + return y > lineTop + relativeOffset + && y < lineBottom + relativeOffset + && x >= lineStart + && x <= lineEnd + } + + fun isTouchY(y: Float, relativeOffset: Float): Boolean { + return y > lineTop + relativeOffset + && y < lineBottom + relativeOffset + } + + fun isVisible(relativeOffset: Float): Boolean { + val top = lineTop + relativeOffset + val bottom = lineBottom + relativeOffset + val width = bottom - top + val visibleTop = ChapterProvider.paddingTop + val visibleBottom = ChapterProvider.visibleBottom + val visible = when { + // 完全可视 + top >= visibleTop && bottom <= visibleBottom -> true + top <= visibleTop && bottom >= visibleBottom -> true + // 上方第一行部分可视 + top < visibleTop && bottom > visibleTop && bottom < visibleBottom -> { + if (isImage) { + true + } else { + val visibleRate = (bottom - visibleTop) / width + visibleRate > 0.6 + } + } + // 下方第一行部分可视 + top > visibleTop && top < visibleBottom && bottom > visibleBottom -> { + if (isImage) { + true + } else { + val visibleRate = (visibleBottom - top) / width + visibleRate > 0.6 + } + } + // 不可视 + else -> false + } + return visible + } + + fun draw(view: ContentTextView, canvas: Canvas) { + if (AppConfig.optimizeRender) { + canvasRecorder.recordIfNeededThenDraw(canvas, view.width, height.toInt()) { + drawTextLine(view, this) + } + } else { + drawTextLine(view, canvas) + } + } + + private fun drawTextLine(view: ContentTextView, canvas: Canvas) { + if (checkFastDraw()) { + fastDrawTextLine(view, canvas) + } else { + for (i in columns.indices) { + columns[i].draw(view, canvas) + } + } + + // 墨水屏模式下的朗读和搜索下划线 + if (AppConfig.isEInkMode && (isReadAloud || searchResultColumnCount > 0)) { + val underlinePaint = PaintPool.obtain() + underlinePaint.set(ChapterProvider.contentPaint) + underlinePaint.strokeWidth = 1.dpToPx().toFloat() + val lineY = height - 1.dpToPx() + canvas.drawLine(lineStart + indentWidth, lineY, lineEnd, lineY, underlinePaint) + PaintPool.recycle(underlinePaint) + } + + if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) { + drawUnderline(canvas) + } + } + + @SuppressLint("NewApi") + private fun fastDrawTextLine(view: ContentTextView, canvas: Canvas) { + val textPaint = if (isTitle) { + ChapterProvider.titlePaint + } else { + ChapterProvider.contentPaint + } + val textColor = if (isReadAloud) { + ThemeStore.accentColor + } else { + ReadBookConfig.textColor + } + if (textPaint.color != textColor) { + textPaint.color = textColor + } + val paint = PaintPool.obtain() + paint.set(textPaint) + val letterSpacing = paint.letterSpacing * paint.textSize + val letterSpacingHalf = letterSpacing * 0.5f + if (extraLetterSpacing != 0f) { + paint.letterSpacing += extraLetterSpacing + } + if (wordSpacing != 0f) { + paint.wordSpacing = wordSpacing + } + val offsetX = if (atLeastApi35) letterSpacingHalf else extraLetterSpacingOffsetX + canvas.drawText(text, indentSize, text.length, startX + offsetX, lineBase - lineTop, paint) + PaintPool.recycle(paint) + for (i in columns.indices) { + val column = columns[i] as TextColumn + if (column.selected) { + canvas.drawRect(column.start, 0f, column.end, height, view.selectedPaint) + } + } + } + + /** + * 绘制下划线 + */ + private fun drawUnderline(canvas: Canvas) { + val lineY = height - 1.dpToPx() + canvas.drawLine( + lineStart + indentWidth, + lineY, + lineEnd, + lineY, + ChapterProvider.contentPaint + ) + } + + fun checkFastDraw(): Boolean { + if (!AppConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) { + return false + } + if (wordSpacing != 0f && (!atLeastApi26 || !wordSpacingWorking)) { + return false + } + return searchResultColumnCount == 0 + } + + fun invalidate() { + invalidateSelf() + textPage.invalidate() + } + + fun invalidateSelf() { + canvasRecorder.invalidate() + } + + fun recycleRecorder() { + canvasRecorder.recycle() + } + + @SuppressLint("NewApi") + companion object { + val emptyTextLine = TextLine() + private val atLeastApi26 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + private val atLeastApi35 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM + private val wordSpacingWorking by lazy { + // issue 3785 3846 + val paint = PaintPool.obtain() + val text = "一二 三" + val width1 = paint.measureText(text) + try { + paint.wordSpacing = 10f + val width2 = paint.measureText(text) + width2 - width1 == 10f + } catch (e: NoSuchMethodError) { + false + } finally { + PaintPool.recycle(paint) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt new file mode 100755 index 000000000..40bfebede --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt @@ -0,0 +1,373 @@ +package io.legado.app.ui.book.read.page.entities + +import android.graphics.Canvas +import android.graphics.Paint +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import androidx.annotation.Keep +import androidx.core.graphics.withTranslation +import io.legado.app.R +import io.legado.app.help.PaintPool +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextChapter.Companion.emptyTextChapter +import io.legado.app.ui.book.read.page.entities.column.TextColumn +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.canvasrecorder.recordIfNeeded +import io.legado.app.utils.dpToPx +import splitties.init.appCtx +import java.text.DecimalFormat +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min + +/** + * 页面信息 + */ +@Keep +@Suppress("unused", "MemberVisibilityCanBePrivate") +data class TextPage( + var index: Int = 0, + var text: String = appCtx.getString(R.string.data_loading), + var title: String = appCtx.getString(R.string.data_loading), + private val textLines: ArrayList = arrayListOf(), + var chapterSize: Int = 0, + var chapterIndex: Int = 0, + var height: Float = 0f, + var leftLineSize: Int = 0, + var renderHeight: Int = 0 +) { + + companion object { + val readProgressFormatter = DecimalFormat("0.0%") + val emptyTextPage = TextPage() + } + + val lines: List get() = textLines + val lineSize: Int get() = textLines.size + val charSize: Int get() = text.length.coerceAtLeast(1) + val chapterPosition: Int get() = textLines.first().chapterPosition + val searchResult = hashSetOf() + var isMsgPage: Boolean = false + var canvasRecorder = CanvasRecorderFactory.create(true) + var doublePage = false + var paddingTop = ChapterProvider.paddingTop + var isCompleted = false + var hasReadAloudSpan = false + + @JvmField + var textChapter = emptyTextChapter + val pageSize get() = textChapter.pageSize + + val paragraphs by lazy { + paragraphsInternal + } + + val paragraphsInternal: ArrayList + get() { + val paragraphs = arrayListOf() + val lines = textLines.filter { it.paragraphNum > 0 } + val offset = lines.first().paragraphNum - 1 + lines.forEach { line -> + if (paragraphs.lastIndex < line.paragraphNum - offset - 1) { + paragraphs.add(TextParagraph(0)) + } + paragraphs[line.paragraphNum - offset - 1].textLines.add(line) + } + return paragraphs + } + + fun addLine(line: TextLine) { + line.textPage = this + textLines.add(line) + } + + fun getLine(index: Int): TextLine { + return textLines.getOrElse(index) { + textLines.last() + } + } + + /** + * 底部对齐更新行位置 + */ + fun upLinesPosition() { + if (!ReadBookConfig.textBottomJustify) return + if (textLines.size <= 1) return + if (leftLineSize == 0) { + leftLineSize = lineSize + } + ChapterProvider.run { + val lastLine = textLines[leftLineSize - 1] + if (lastLine.isImage) return@run + val lastLineHeight = with(lastLine) { lineBottom - lineTop } + val pageHeight = lastLine.lineBottom + contentPaintTextHeight * lineSpacingExtra + if (visibleHeight - pageHeight >= lastLineHeight) return@run + val surplus = (visibleBottom - lastLine.lineBottom) + if (surplus == 0f) return@run + height += surplus + val tj = surplus / (leftLineSize - 1) + for (i in 1 until leftLineSize) { + val line = textLines[i] + line.lineTop += tj * i + line.lineBase += tj * i + line.lineBottom += tj * i + } + } + if (leftLineSize == lineSize) return + ChapterProvider.run { + val lastLine = textLines.last() + if (lastLine.isImage) return@run + val lastLineHeight = with(lastLine) { lineBottom - lineTop } + val pageHeight = lastLine.lineBottom + contentPaintTextHeight * lineSpacingExtra + if (visibleHeight - pageHeight >= lastLineHeight) return@run + val surplus = (visibleBottom - lastLine.lineBottom) + if (surplus == 0f) return@run + val tj = surplus / (textLines.size - leftLineSize - 1) + for (i in leftLineSize + 1 until textLines.size) { + val line = textLines[i] + val surplusIndex = i - leftLineSize + line.lineTop += tj * surplusIndex + line.lineBase += tj * surplusIndex + line.lineBottom += tj * surplusIndex + } + } + } + + /** + * 计算文字位置,只用作单页面内容 + */ + @Suppress("DEPRECATION") + fun format(): TextPage { + if (textLines.isEmpty()) isMsgPage = true + if (isMsgPage && ChapterProvider.viewWidth > 0) { + textLines.clear() + val visibleWidth = ChapterProvider.visibleRight - ChapterProvider.paddingLeft + val paint = ChapterProvider.contentPaint + val layout = StaticLayout( + text, paint, visibleWidth, + Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false + ) + val letterSpacing = paint.letterSpacing * paint.textSize + var y = (ChapterProvider.visibleHeight - layout.height) / 2f + if (y < 0) y = 0f + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine() + textLine.lineTop = ChapterProvider.paddingTop + y + layout.getLineTop(lineIndex) + textLine.lineBase = + ChapterProvider.paddingTop + y + layout.getLineBaseline(lineIndex) + textLine.lineBottom = + ChapterProvider.paddingTop + y + layout.getLineBottom(lineIndex) + var x = ChapterProvider.paddingLeft + + (visibleWidth - layout.getLineMax(lineIndex)) / 2 + textLine.text = + text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) + for (i in textLine.text.indices) { + val char = textLine.text[i].toString() + var cw = StaticLayout.getDesiredWidth(char, paint) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + cw += letterSpacing + } + val x1 = x + cw + textLine.addColumn( + TextColumn(start = x, end = x1, char) + ) + x = x1 + } + addLine(textLine) + } + height = ChapterProvider.visibleHeight.toFloat() + upRenderHeight() + invalidate() + isCompleted = true + } + return this + } + + /** + * 移除朗读标志 + */ + fun removePageAloudSpan(): TextPage { + if (!hasReadAloudSpan) { + return this + } + hasReadAloudSpan = false + for (i in textLines.indices) { + textLines[i].isReadAloud = false + } + return this + } + + /** + * 更新朗读标志 + * @param aloudSpanStart 朗读文字开始位置 + */ + fun upPageAloudSpan(aloudSpanStart: Int) { + removePageAloudSpan() + var lineStart = 0 + for (index in textLines.indices) { + val textLine = textLines[index] + val lineLength = textLine.text.length + if (textLine.isParagraphEnd) 1 else 0 + if (aloudSpanStart >= lineStart && aloudSpanStart < lineStart + lineLength) { + for (i in index - 1 downTo 0) { + if (textLines[i].isParagraphEnd) { + break + } else { + textLines[i].isReadAloud = true + } + } + for (i in index until textLines.size) { + if (textLines[i].isParagraphEnd) { + textLines[i].isReadAloud = true + break + } else { + textLines[i].isReadAloud = true + } + } + break + } + lineStart += lineLength + } + } + + /** + * 阅读进度 + */ + val readProgress: String + get() { + val df = readProgressFormatter + if (chapterSize == 0 || pageSize == 0 && chapterIndex == 0) { + return "0.0%" + } else if (pageSize == 0) { + return df.format((chapterIndex + 1.0f) / chapterSize.toDouble()) + } + var percent = + df.format(chapterIndex * 1.0f / chapterSize + 1.0f / chapterSize * (index + 1) / pageSize.toDouble()) + if (percent == "100.0%" && (chapterIndex + 1 != chapterSize || index + 1 != pageSize)) { + percent = "99.9%" + } + return percent + } + + /** + * 根据行和列返回字符在本页的位置 + * @param lineIndex 字符在第几行 + * @param columnIndex 字符在第几列 + * @return 字符在本页位置 + */ + fun getPosByLineColumn(lineIndex: Int, columnIndex: Int): Int { + var length = 0 + val maxIndex = min(lineIndex, lineSize - 1) + for (index in 0 until maxIndex) { + length += textLines[index].charSize + if (textLines[index].isParagraphEnd) { + length++ + } + } + val columns = textLines[maxIndex].columns + for (index in 0 until columnIndex) { + val column = columns[index] + if (column is TextColumn) { + length += column.charData.length + } + } + return length + } + + /** + * @return 页面所在章节 + */ + fun getTextChapter(): TextChapter { + return textChapter + } + + /** + * 判断章节字符位置是否在这一页中 + * + * @param chapterPos 章节字符位置 + * @return + */ + fun containPos(chapterPos: Int): Boolean { + val line = lines.first() + val startPos = line.chapterPosition + val endPos = startPos + charSize + return chapterPos in startPos.. 0 && leftLineSize != lines.size) { + val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt() + renderHeight = max(renderHeight, leftHeight) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextParagraph.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextParagraph.kt new file mode 100755 index 000000000..9af890ab5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextParagraph.kt @@ -0,0 +1,17 @@ +package io.legado.app.ui.book.read.page.entities + +@Suppress("unused", "MemberVisibilityCanBePrivate") +data class TextParagraph( + var num: Int, + val textLines: ArrayList = arrayListOf(), +) { + val text: String get() = textLines.joinToString("") { it.text } + val length: Int get() = text.length + val firstLine: TextLine get() = textLines.first() + val lastLine: TextLine get() = textLines.last() + val chapterIndices: IntRange get() = firstLine.chapterPosition..lastLine.chapterPosition + lastLine.charSize + val chapterPosition: Int get() = firstLine.chapterPosition + val realNum: Int get() = firstLine.paragraphNum + val isParagraphEnd: Boolean get() = lastLine.isParagraphEnd + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt new file mode 100755 index 000000000..e3599f20d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt @@ -0,0 +1,66 @@ +package io.legado.app.ui.book.read.page.entities + +import androidx.annotation.Keep + +/** + * 位置信息 + */ +@Keep +@Suppress("unused") +data class TextPos( + var relativePagePos: Int, + var lineIndex: Int, + var columnIndex: Int, +) { + + fun upData( + relativePos: Int, + lineIndex: Int, + charIndex: Int, + ) { + this.relativePagePos = relativePos + this.lineIndex = lineIndex + this.columnIndex = charIndex + } + + fun upData(pos: TextPos) { + relativePagePos = pos.relativePagePos + lineIndex = pos.lineIndex + columnIndex = pos.columnIndex + } + + fun compare(pos: TextPos): Int { + return when { + relativePagePos < pos.relativePagePos -> -3 + relativePagePos > pos.relativePagePos -> 3 + lineIndex < pos.lineIndex -> -2 + lineIndex > pos.lineIndex -> 2 + columnIndex < pos.columnIndex -> -1 + columnIndex > pos.columnIndex -> 1 + else -> 0 + } + } + + fun compare(relativePos: Int, lineIndex: Int, charIndex: Int): Int { + return when { + this.relativePagePos < relativePos -> -3 + this.relativePagePos > relativePos -> 3 + this.lineIndex < lineIndex -> -2 + this.lineIndex > lineIndex -> 2 + this.columnIndex < charIndex -> -1 + this.columnIndex > charIndex -> 1 + else -> 0 + } + } + + fun reset() { + relativePagePos = 0 + lineIndex = -1 + columnIndex = -1 + } + + fun isSelected(): Boolean { + return lineIndex >= 0 && columnIndex >= 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt new file mode 100755 index 000000000..ed86cd153 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/BaseColumn.kt @@ -0,0 +1,21 @@ +package io.legado.app.ui.book.read.page.entities.column + +import android.graphics.Canvas +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextLine + +/** + * 列基类 + */ +interface BaseColumn { + var start: Float + var end: Float + var textLine: TextLine + + fun draw(view: ContentTextView, canvas: Canvas) + + fun isTouch(x: Float): Boolean { + return x > start && x < end + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt new file mode 100755 index 000000000..abb3ac42a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ButtonColumn.kt @@ -0,0 +1,22 @@ +package io.legado.app.ui.book.read.page.entities.column + +import android.graphics.Canvas +import androidx.annotation.Keep +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine + + +/** + * 按钮列 + */ +@Keep +data class ButtonColumn( + override var start: Float, + override var end: Float, +) : BaseColumn { + override var textLine: TextLine = emptyTextLine + override fun draw(view: ContentTextView, canvas: Canvas) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt new file mode 100755 index 000000000..084d8d320 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ImageColumn.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.book.read.page.entities.column + +import android.graphics.Canvas +import android.graphics.RectF +import androidx.annotation.Keep +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +/** + * 图片列 + */ +@Keep +data class ImageColumn( + override var start: Float, + override var end: Float, + var src: String +) : BaseColumn { + + override var textLine: TextLine = emptyTextLine + override fun draw(view: ContentTextView, canvas: Canvas) { + val book = ReadBook.book ?: return + + val height = textLine.height + + val bitmap = ImageProvider.getImage( + book, + src, + (end - start).toInt(), + height.toInt() + ) + + val rectF = if (textLine.isImage) { + RectF(start, 0f, end, height) + } else { + /*以宽度为基准保持图片的原始比例叠加,当div为负数时,允许高度比字符更高*/ + val h = (end - start) / bitmap.width * bitmap.height + val div = (height - h) / 2 + RectF(start, div, end, height - div) + } + kotlin.runCatching { + canvas.drawBitmap(bitmap, null, rectF, view.imagePaint) + }.onFailure { e -> + appCtx.toastOnUi(e.localizedMessage) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt new file mode 100755 index 000000000..c1612fb2f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/ReviewColumn.kt @@ -0,0 +1,65 @@ +package io.legado.app.ui.book.read.page.entities.column + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import androidx.annotation.Keep +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine +import io.legado.app.ui.book.read.page.provider.ChapterProvider + +/** + * 评论按钮列 + */ +@Keep +data class ReviewColumn( + override var start: Float, + override var end: Float, + val count: Int = 0 +) : BaseColumn { + + override var textLine: TextLine = emptyTextLine + override fun draw(view: ContentTextView, canvas: Canvas) { + val textPaint = if (textLine.isTitle) { + ChapterProvider.titlePaint + } else { + ChapterProvider.contentPaint + } + drawToCanvas(canvas, textLine.lineBase, textPaint.textSize) + } + + val countText by lazy { + if (count > 999) { + return@lazy "999" + } + return@lazy count.toString() + } + + val path by lazy { Path() } + + fun drawToCanvas(canvas: Canvas, baseLine: Float, height: Float) { + if (count == 0) return + path.reset() + path.moveTo(start + 1, baseLine - height * 2 / 5) + path.lineTo(start + height / 6, baseLine - height * 0.55f) + path.lineTo(start + height / 6, baseLine - height * 0.8f) + path.lineTo(end - 1, baseLine - height * 0.8f) + path.lineTo(end - 1, baseLine) + path.lineTo(start + height / 6, baseLine) + path.lineTo(start + height / 6, baseLine - height / 4) + path.close() + val reviewPaint = ChapterProvider.reviewPaint + reviewPaint.style = Paint.Style.STROKE + canvas.drawPath(path, reviewPaint) + reviewPaint.style = Paint.Style.FILL + canvas.drawText( + countText, + (start + height / 9 + end) / 2, + baseLine - height * 0.23f, + reviewPaint + ) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt new file mode 100755 index 000000000..4097ae30e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt @@ -0,0 +1,72 @@ +package io.legado.app.ui.book.read.page.entities.column + +import android.graphics.Canvas +import android.os.Build +import androidx.annotation.Keep +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.ui.book.read.page.ContentTextView +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine +import io.legado.app.ui.book.read.page.provider.ChapterProvider + +/** + * 文字列 + */ +@Keep +data class TextColumn( + override var start: Float, + override var end: Float, + val charData: String, +) : BaseColumn { + + override var textLine: TextLine = emptyTextLine + + var selected: Boolean = false + set(value) { + if (field != value) { + textLine.invalidate() + } + field = value + } + var isSearchResult: Boolean = false + set(value) { + if (field != value) { + textLine.invalidate() + if (value) { + textLine.searchResultColumnCount++ + } else { + textLine.searchResultColumnCount-- + } + } + field = value + } + + override fun draw(view: ContentTextView, canvas: Canvas) { + val textPaint = if (textLine.isTitle) { + ChapterProvider.titlePaint + } else { + ChapterProvider.contentPaint + } + val textColor = if (textLine.isReadAloud || isSearchResult) { + ThemeStore.accentColor + } else { + ReadBookConfig.textColor + } + if (textPaint.color != textColor) { + textPaint.color = textColor + } + val y = textLine.lineBase - textLine.lineTop + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + val letterSpacing = textPaint.letterSpacing * textPaint.textSize + val letterSpacingHalf = letterSpacing * 0.5f + canvas.drawText(charData, start + letterSpacingHalf, y, textPaint) + } else { + canvas.drawText(charData, start, y, textPaint) + } + if (selected) { + canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt new file mode 100755 index 000000000..a25527e81 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -0,0 +1,1053 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Paint.FontMetrics +import android.graphics.RectF +import android.graphics.Typeface +import android.net.Uri +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import androidx.core.os.postDelayed +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.EventBus +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.BookContent +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.entities.column.ImageColumn +import io.legado.app.ui.book.read.page.entities.column.ReviewColumn +import io.legado.app.ui.book.read.page.entities.column.TextColumn +import io.legado.app.utils.RealPathUtil +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.dpToPx +import io.legado.app.utils.fastSum +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.isPad +import io.legado.app.utils.postEvent +import io.legado.app.utils.spToPx +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.textHeight +import kotlinx.coroutines.CoroutineScope +import splitties.init.appCtx +import java.util.LinkedList +import java.util.Locale + +/** + * 解析内容生成章节和页面 + */ +@Suppress("DEPRECATION", "ConstPropertyName") +object ChapterProvider { + //用于图片字的替换 + const val srcReplaceChar = "▩" + + //用于评论按钮的替换 + const val reviewChar = "▨" + + const val indentChar = " " + + @JvmStatic + var viewWidth = 0 + private set + + @JvmStatic + var viewHeight = 0 + private set + + @JvmStatic + var paddingLeft = 0 + private set + + @JvmStatic + var paddingTop = 0 + private set + + @JvmStatic + var paddingRight = 0 + private set + + @JvmStatic + var paddingBottom = 0 + private set + + @JvmStatic + var visibleWidth = 0 + private set + + @JvmStatic + var visibleHeight = 0 + private set + + @JvmStatic + var visibleRight = 0 + private set + + @JvmStatic + var visibleBottom = 0 + private set + + @JvmStatic + var lineSpacingExtra = 0f + private set + + @JvmStatic + var paragraphSpacing = 0 + private set + + @JvmStatic + var titleTopSpacing = 0 + private set + + @JvmStatic + var titleBottomSpacing = 0 + private set + + @JvmStatic + var indentCharWidth = 0f + private set + + @JvmStatic + var titlePaintTextHeight = 0f + private set + + @JvmStatic + var contentPaintTextHeight = 0f + private set + + @JvmStatic + var titlePaintFontMetrics = FontMetrics() + + @JvmStatic + var contentPaintFontMetrics = FontMetrics() + + @JvmStatic + var typeface: Typeface? = Typeface.DEFAULT + private set + + @JvmStatic + var titlePaint: TextPaint = TextPaint() + + @JvmStatic + var contentPaint: TextPaint = TextPaint() + + @JvmStatic + var reviewPaint: TextPaint = TextPaint() + + @JvmStatic + var doublePage = false + private set + + @JvmStatic + var visibleRect = RectF() + + private val handler by lazy { + buildMainHandler() + } + + private var upViewSizeRunnable: Runnable? = null + + init { + upStyle() + } + + /** + * 获取拆分完的章节数据 + */ + suspend fun getTextChapter( + book: Book, + bookChapter: BookChapter, + displayTitle: String, + bookContent: BookContent, + chapterSize: Int, + ): TextChapter { + val contents = bookContent.textList + val textPages = arrayListOf() + val stringBuilder = StringBuilder() + var absStartX = paddingLeft + var durY = 0f + textPages.add(TextPage()) + if (ReadBookConfig.titleMode != 2 || bookChapter.isVolume) { + //标题非隐藏 + displayTitle.splitNotBlank("\n").forEach { text -> + setTypeText( + book, absStartX, durY, + if (AppConfig.enableReview) text + reviewChar else text, + textPages, + stringBuilder, + titlePaint, + titlePaintTextHeight, + titlePaintFontMetrics, + isTitle = true, + emptyContent = contents.isEmpty(), + isVolumeTitle = bookChapter.isVolume + ).let { + absStartX = it.first + durY = it.second + } + } + textPages.last().lines.last().isParagraphEnd = true + stringBuilder.append("\n") + durY += titleBottomSpacing + } + contents.forEach { content -> + if (book.getImageStyle().equals(Book.imgStyleText, true)) { + //图片样式为文字嵌入类型 + var text = content.replace(srcReplaceChar, "▣") + val srcList = LinkedList() + val sb = StringBuffer() + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { src -> + srcList.add(src) + matcher.appendReplacement(sb, srcReplaceChar) + } + } + matcher.appendTail(sb) + text = sb.toString() + setTypeText( + book, + absStartX, + durY, + text, + textPages, + stringBuilder, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics, + srcList = srcList + ).let { + absStartX = it.first + durY = it.second + } + } else { + val matcher = AppPattern.imgPattern.matcher(content) + var start = 0 + while (matcher.find()) { + val text = content.substring(start, matcher.start()) + if (text.isNotBlank()) { + setTypeText( + book, + absStartX, + durY, + text, + textPages, + stringBuilder, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics + ).let { + absStartX = it.first + durY = it.second + } + } + setTypeImage( + book, + matcher.group(1)!!, + absStartX, + durY, + textPages, + contentPaintTextHeight, + stringBuilder, + book.getImageStyle() + ).let { + absStartX = it.first + durY = it.second + } + start = matcher.end() + } + if (start < content.length) { + val text = content.substring(start, content.length) + if (text.isNotBlank()) { + setTypeText( + book, absStartX, durY, + if (AppConfig.enableReview) text + reviewChar else text, + textPages, + stringBuilder, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics + ).let { + absStartX = it.first + durY = it.second + } + } + } + } + textPages.last().lines.last().isParagraphEnd = true + stringBuilder.append("\n") + } + val textPage = textPages.last() + val endPadding = 20.dpToPx() + val durYPadding = durY + endPadding + if (textPage.height < durYPadding) { + textPage.height = durYPadding + } else { + textPage.height += endPadding + } + textPage.text = stringBuilder.toString() + textPages.forEachIndexed { index, item -> + item.index = index + //item.pageSize = textPages.size + item.chapterIndex = bookChapter.index + item.chapterSize = chapterSize + item.title = displayTitle + item.doublePage = doublePage + item.paddingTop = paddingTop + item.upLinesPosition() + } + + return TextChapter( + bookChapter, + bookChapter.index, displayTitle, + //textPages, + chapterSize, + bookContent.sameTitleRemoved, + bookChapter.isVip, + bookChapter.isPay, + bookContent.effectiveReplaceRules + ) + } + + fun getTextChapterAsync( + scope: CoroutineScope, + book: Book, + bookChapter: BookChapter, + displayTitle: String, + bookContent: BookContent, + chapterSize: Int, + ): TextChapter { + + val textChapter = TextChapter( + bookChapter, + bookChapter.index, displayTitle, + chapterSize, + bookContent.sameTitleRemoved, + bookChapter.isVip, + bookChapter.isPay, + bookContent.effectiveReplaceRules + ).apply { + createLayout(scope, book, bookContent) + } + + return textChapter + } + + /** + * 排版图片 + */ + private suspend fun setTypeImage( + book: Book, + src: String, + x: Int, + y: Float, + textPages: ArrayList, + textHeight: Float, + stringBuilder: StringBuilder, + imageStyle: String?, + ): Pair { + var absStartX = x + var durY = y + val size = ImageProvider.getImageSize(book, src, ReadBook.bookSource) + if (size.width > 0 && size.height > 0) { + if (durY > visibleHeight) { + val textPage = textPages.last() + if (textPage.height < durY) { + textPage.height = durY + } + textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } + stringBuilder.clear() + textPages.add(TextPage()) + durY = 0f + } + var height = size.height + var width = size.width + when (imageStyle?.uppercase(Locale.ROOT)) { + Book.imgStyleFull -> { + width = visibleWidth + height = size.height * visibleWidth / size.width + } + + Book.imgStyleSingle -> { + width = visibleWidth + height = size.height * visibleWidth / size.width + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + if (durY > 0f) { + val textPage = textPages.last() + if (doublePage && absStartX < viewWidth / 2) { + //当前页面左列结束 + textPage.leftLineSize = textPage.lineSize + absStartX = viewWidth / 2 + paddingLeft + } else { + //当前页面结束 + if (textPage.leftLineSize == 0) { + textPage.leftLineSize = textPage.lineSize + } + textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } + stringBuilder.clear() + textPages.add(TextPage()) + } + // 双页的 durY 不正确,可能会小于实际高度 + if (textPage.height < durY) { + textPage.height = durY + } + durY = 0f + } + + // 图片竖直方向居中:调整 Y 坐标 + if (height < visibleHeight) { + val adjustHeight = (visibleHeight - height) / 2f + durY = adjustHeight // 将 Y 坐标设置为居中位置 + } + } + + else -> { + if (size.width > visibleWidth) { + height = size.height * visibleWidth / size.width + width = visibleWidth + } + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + if (durY + height > visibleHeight) { + val textPage = textPages.last() + if (doublePage && absStartX < viewWidth / 2) { + //当前页面左列结束 + textPage.leftLineSize = textPage.lineSize + absStartX = viewWidth / 2 + paddingLeft + } else { + //当前页面结束 + if (textPage.leftLineSize == 0) { + textPage.leftLineSize = textPage.lineSize + } + textPage.text = stringBuilder.toString().ifEmpty { "本页无文字内容" } + stringBuilder.clear() + textPages.add(TextPage()) + } + // 双页的 durY 不正确,可能会小于实际高度 + if (textPage.height < durY) { + textPage.height = durY + } + durY = 0f + } + } + } + val textLine = TextLine(isImage = true) + textLine.lineTop = durY + paddingTop + durY += height + textLine.lineBottom = durY + paddingTop + val (start, end) = if (visibleWidth > width) { + val adjustWidth = (visibleWidth - width) / 2f + Pair(adjustWidth, adjustWidth + width) + } else { + Pair(0f, width.toFloat()) + } + textLine.addColumn( + ImageColumn(start = x + start, end = x + end, src = src) + ) + calcTextLinePosition(textPages, textLine, stringBuilder.length) + stringBuilder.append(" ") // 确保翻页时索引计算正确 + textPages.last().addLine(textLine) + } + return absStartX to durY + textHeight * paragraphSpacing / 10f + } + + /** + * 排版文字 + */ + private suspend fun setTypeText( + book: Book, + x: Int, + y: Float, + text: String, + textPages: ArrayList, + stringBuilder: StringBuilder, + textPaint: TextPaint, + textHeight: Float, + fontMetrics: FontMetrics, + isTitle: Boolean = false, + emptyContent: Boolean = false, + isVolumeTitle: Boolean = false, + srcList: LinkedList? = null + ): Pair { + var absStartX = x + val layout = if (ReadBookConfig.useZhLayout) { + ZhLayout( + text, textPaint, visibleWidth, emptyList(), emptyList(), + ReadBookConfig.paragraphIndent.length + ) + } else { + StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true) + } + var durY = when { + //标题y轴居中 + emptyContent && textPages.size == 1 -> { + val textPage = textPages.last() + if (textPage.lineSize == 0) { + val ty = (visibleHeight - layout.lineCount * textHeight) / 2 + if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat() + } else { + var textLayoutHeight = layout.lineCount * textHeight + val fistLine = textPage.getLine(0) + if (fistLine.lineTop < textLayoutHeight + titleTopSpacing) { + textLayoutHeight = fistLine.lineTop - titleTopSpacing + } + textPage.lines.forEach { + it.lineTop -= textLayoutHeight + it.lineBase -= textLayoutHeight + it.lineBottom -= textLayoutHeight + } + y - textLayoutHeight + } + } + + isTitle && textPages.size == 1 && textPages.last().lines.isEmpty() -> + y + titleTopSpacing + + else -> y + } + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine(isTitle = isTitle) + if (durY + textHeight > visibleHeight) { + val textPage = textPages.last() + if (doublePage && absStartX < viewWidth / 2) { + //当前页面左列结束 + textPage.leftLineSize = textPage.lineSize + absStartX = viewWidth / 2 + paddingLeft + } else { + //当前页面结束,设置各种值 + if (textPage.leftLineSize == 0) { + textPage.leftLineSize = textPage.lineSize + } + textPage.text = stringBuilder.toString() + //新建页面 + textPages.add(TextPage()) + stringBuilder.clear() + absStartX = paddingLeft + } + if (textPage.height < durY) { + textPage.height = durY + } + durY = 0f + } + val lineStart = layout.getLineStart(lineIndex) + val lineEnd = layout.getLineEnd(lineIndex) + val lineText = text.substring(lineStart, lineEnd) + val (words, widths) = measureTextSplit(lineText, textPaint) + val desiredWidth = widths.fastSum() + when { + lineIndex == 0 && layout.lineCount > 1 && !isTitle -> { + //第一行 非标题 + textLine.text = lineText + addCharsToLineFirst( + book, absStartX, textLine, words, + desiredWidth, widths, srcList + ) + } + + lineIndex == layout.lineCount - 1 -> { + //最后一行 + textLine.text = lineText + //标题x轴居中 + val startX = if ( + isTitle && + (ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle) + ) { + (visibleWidth - desiredWidth) / 2 + } else { + 0f + } + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, !isTitle && lineIndex == 0, widths, srcList + ) + } + + else -> { + if ( + isTitle && + (ReadBookConfig.isMiddleTitle || emptyContent || isVolumeTitle) + ) { + //标题居中 + val startX = (visibleWidth - desiredWidth) / 2 + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, false, widths, srcList + ) + } else { + //中间行 + textLine.text = lineText + addCharsToLineMiddle( + book, absStartX, textLine, words, + desiredWidth, 0f, widths, srcList + ) + } + } + } + if (doublePage) { + textLine.isLeftLine = absStartX < viewWidth / 2 + } + calcTextLinePosition(textPages, textLine, stringBuilder.length) + stringBuilder.append(lineText) + textLine.upTopBottom(durY, textHeight, fontMetrics) + val textPage = textPages.last() + textPage.addLine(textLine) + durY += textHeight * lineSpacingExtra + if (textPage.height < durY) { + textPage.height = durY + } + } + durY += textHeight * paragraphSpacing / 10f + return Pair(absStartX, durY) + } + + private fun calcTextLinePosition( + textPages: ArrayList, + textLine: TextLine, + sbLength: Int + ) { + val lastLine = textPages.last().lines.lastOrNull { it.paragraphNum > 0 } + ?: textPages.getOrNull( + textPages.lastIndex - 1 + )?.lines?.lastOrNull { it.paragraphNum > 0 } + val paragraphNum = when { + lastLine == null -> 1 + lastLine.isParagraphEnd -> lastLine.paragraphNum + 1 + else -> lastLine.paragraphNum + } + textLine.paragraphNum = paragraphNum + textLine.chapterPosition = + (textPages.getOrNull(textPages.lastIndex - 1)?.lines?.lastOrNull()?.run { + chapterPosition + charSize + if (isParagraphEnd) 1 else 0 + } ?: 0) + sbLength + textLine.pagePosition = sbLength + } + + /** + * 有缩进,两端对齐 + */ + private suspend fun addCharsToLineFirst( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + /**自然排版长度**/ + desiredWidth: Float, + textWidths: List, + srcList: LinkedList? + ) { + var x = 0f + if (!ReadBookConfig.textFullJustify) { + addCharsToLineNatural( + book, absStartX, textLine, words, + x, true, textWidths, srcList + ) + return + } + val bodyIndent = ReadBookConfig.paragraphIndent + for (i in bodyIndent.indices) { + val x1 = x + indentCharWidth + textLine.addColumn( + TextColumn( + charData = indentChar, + start = absStartX + x, + end = absStartX + x1 + ) + ) + x = x1 + textLine.indentWidth = x + } + if (words.size > bodyIndent.length) { + val text1 = words.subList(bodyIndent.length, words.size) + val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size) + addCharsToLineMiddle( + book, absStartX, textLine, text1, + desiredWidth, x, textWidths1, srcList + ) + } + } + + /** + * 无缩进,两端对齐 + */ + private suspend fun addCharsToLineMiddle( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + /**自然排版长度**/ + desiredWidth: Float, + /**起始x坐标**/ + startX: Float, + textWidths: List, + srcList: LinkedList? + ) { + if (!ReadBookConfig.textFullJustify) { + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, false, textWidths, srcList + ) + return + } + val residualWidth = visibleWidth - desiredWidth + val spaceSize = words.count { it == " " } + if (spaceSize > 1) { + val d = residualWidth / spaceSize + var x = startX + for (index in words.indices) { + val char = words[index] + val cw = textWidths[index] + val x1 = if (char == " ") { + if (index != words.lastIndex) (x + cw + d) else (x + cw) + } else { + (x + cw) + } + addCharToLine( + book, absStartX, textLine, char, + x, x1, index + 1 == words.size, srcList + ) + x = x1 + } + } else { + val gapCount: Int = words.lastIndex + val d = residualWidth / gapCount + var x = startX + for (index in words.indices) { + val char = words[index] + val cw = textWidths[index] + val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) + addCharToLine( + book, absStartX, textLine, char, + x, x1, index + 1 == words.size, srcList + ) + x = x1 + } + } + exceed(absStartX, textLine, words) + } + + /** + * 自然排列 + */ + private suspend fun addCharsToLineNatural( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + startX: Float, + hasIndent: Boolean, + textWidths: List, + srcList: LinkedList? + ) { + val indentLength = ReadBookConfig.paragraphIndent.length + var x = startX + for (index in words.indices) { + val char = words[index] + val cw = textWidths[index] + val x1 = x + cw + addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList) + x = x1 + if (hasIndent && index == indentLength - 1) { + textLine.indentWidth = x + } + } + exceed(absStartX, textLine, words) + } + + /** + * 添加字符 + */ + private suspend fun addCharToLine( + book: Book, + absStartX: Int, + textLine: TextLine, + char: String, + xStart: Float, + xEnd: Float, + isLineEnd: Boolean, + srcList: LinkedList? + ) { + val column = when { + srcList != null && char == srcReplaceChar -> { + val src = srcList.removeFirst() + ImageProvider.cacheImage(book, src, ReadBook.bookSource) + ImageColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + src = src + ) + } + + isLineEnd && char == reviewChar -> { + ReviewColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + count = 100 + ) + } + + else -> { + TextColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + charData = char + ) + } + } + textLine.addColumn(column) + } + + /** + * 超出边界处理 + */ + private fun exceed(absStartX: Int, textLine: TextLine, words: List) { + val visibleEnd = absStartX + visibleWidth + val endX = textLine.columns.lastOrNull()?.end ?: return + if (endX > visibleEnd) { + val cc = (endX - visibleEnd) / words.size + for (i in 0..words.lastIndex) { + textLine.getColumnReverseAt(i).let { + val py = cc * (words.size - i) + it.start -= py + it.end -= py + } + } + } + } + + fun measureTextSplit( + text: String, + paint: TextPaint + ): Pair, ArrayList> { + val length = text.length + val widthsArray = FloatArray(length) + paint.getTextWidths(text, widthsArray) + val clusterCount = widthsArray.count { it > 0f } + val widths = ArrayList(clusterCount) + val stringList = ArrayList(clusterCount) + var i = 0 + while (i < length) { + val clusterBaseIndex = i++ + widths.add(widthsArray[clusterBaseIndex]) + while (i < length && widthsArray[i] == 0f) { + i++ + } + stringList.add(text.substring(clusterBaseIndex, i)) + } + return stringList to widths + } + + /** + * 更新样式 + */ + fun upStyle() { + typeface = getTypeface(ReadBookConfig.textFont) + getPaints(typeface).let { + titlePaint = it.first + contentPaint = it.second +// reviewPaint.color = contentPaint.color +// reviewPaint.textSize = contentPaint.textSize * 0.45f +// reviewPaint.textAlign = Paint.Align.CENTER + } + //间距 + lineSpacingExtra = ReadBookConfig.lineSpacingExtra / 10f + paragraphSpacing = ReadBookConfig.paragraphSpacing + titleTopSpacing = ReadBookConfig.titleTopSpacing.dpToPx() + titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dpToPx() + val bodyIndent = ReadBookConfig.paragraphIndent + indentCharWidth = if (bodyIndent.isNotEmpty()) { + var indentWidth = StaticLayout.getDesiredWidth(bodyIndent, contentPaint) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + indentWidth += contentPaint.letterSpacing * contentPaint.textSize + } + indentWidth / bodyIndent.length + } else { + 0f + } + titlePaintTextHeight = titlePaint.textHeight + contentPaintTextHeight = contentPaint.textHeight + titlePaintFontMetrics = titlePaint.fontMetrics + contentPaintFontMetrics = contentPaint.fontMetrics + upLayout() + } + + private fun getTypeface(fontPath: String): Typeface? { + return kotlin.runCatching { + when { + fontPath.isContentScheme() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> { + appCtx.contentResolver + .openFileDescriptor(Uri.parse(fontPath), "r")!! + .use { + Typeface.Builder(it.fileDescriptor).build() + } + } + + fontPath.isContentScheme() -> { + Typeface.createFromFile(RealPathUtil.getPath(appCtx, Uri.parse(fontPath))) + } + + fontPath.isNotEmpty() -> Typeface.createFromFile(fontPath) + else -> when (AppConfig.systemTypefaces) { + 1 -> Typeface.SERIF + 2 -> Typeface.MONOSPACE + else -> Typeface.SANS_SERIF + } + } + }.getOrElse { + ReadBookConfig.textFont = "" + ReadBookConfig.save() + Typeface.SANS_SERIF + } ?: Typeface.DEFAULT + } + + private fun getPaints(typeface: Typeface?): Pair { + // 字体统一处理 + val bold = Typeface.create(typeface, Typeface.BOLD) + val normal = Typeface.create(typeface, Typeface.NORMAL) + val (titleFont, textFont) = when (ReadBookConfig.textBold) { + 1 -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + Pair(Typeface.create(typeface, 900, false), bold) + else + Pair(bold, bold) + } + + 2 -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + Pair(normal, Typeface.create(typeface, 300, false)) + else + Pair(normal, normal) + } + + else -> Pair(bold, normal) + } + + //标题 + val tPaint = TextPaint() + tPaint.color = ReadBookConfig.textColor + tPaint.letterSpacing = ReadBookConfig.letterSpacing + tPaint.typeface = titleFont + tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx() + tPaint.isAntiAlias = true + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && AppConfig.optimizeRender) { + tPaint.isLinearText = true + } + //正文 + val cPaint = TextPaint() + cPaint.color = ReadBookConfig.textColor + cPaint.letterSpacing = ReadBookConfig.letterSpacing + cPaint.typeface = textFont + cPaint.textSize = ReadBookConfig.textSize.toFloat().spToPx() + cPaint.isAntiAlias = true + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && AppConfig.optimizeRender) { + cPaint.isLinearText = true + } + return Pair(tPaint, cPaint) + } + + /** + * 更新View尺寸 + */ + fun upViewSize(width: Int, height: Int) { + if (width <= 0 || height <= 0) { + return + } + if (width != viewWidth || height != viewHeight) { + if (width == viewWidth) { + upViewSizeRunnable = handler.postDelayed(300) { + upViewSizeRunnable = null + notifyViewSizeChange(width, height) + } + } else { + notifyViewSizeChange(width, height) + } + } else if (upViewSizeRunnable != null) { + handler.removeCallbacks(upViewSizeRunnable!!) + upViewSizeRunnable = null + } + } + + private fun notifyViewSizeChange(width: Int, height: Int) { + viewWidth = width + viewHeight = height + upLayout() + postEvent(EventBus.UP_CONFIG, arrayListOf(5)) + } + + /** + * 更新绘制尺寸 + */ + fun upLayout() { + when (AppConfig.doublePageHorizontal) { + "0" -> doublePage = false + "1" -> doublePage = true + "2" -> { + doublePage = (viewWidth > viewHeight) + && ReadBook.pageAnim() != 3 + } + + "3" -> { + doublePage = (viewWidth > viewHeight || appCtx.isPad) + && ReadBook.pageAnim() != 3 + } + } + + if (viewWidth <= 0 || viewHeight <= 0) { + return + } + + paddingLeft = ReadBookConfig.paddingLeft.dpToPx() + paddingTop = ReadBookConfig.paddingTop.dpToPx() + paddingRight = ReadBookConfig.paddingRight.dpToPx() + paddingBottom = ReadBookConfig.paddingBottom.dpToPx() + visibleWidth = if (doublePage) { + viewWidth / 2 - paddingLeft - paddingRight + } else { + viewWidth - paddingLeft - paddingRight + } + //留1dp画最后一行下划线 + visibleHeight = viewHeight - paddingTop - paddingBottom + visibleRight = viewWidth - paddingRight + visibleBottom = paddingTop + visibleHeight + + if (paddingLeft >= visibleRight || paddingTop >= visibleBottom) { + AppLog.put("边距设置过大,请重新设置", toast = true) + setFallbackLayout() + } + + visibleRect.set( + paddingLeft.toFloat(), + paddingTop.toFloat(), + visibleRight.toFloat(), + visibleBottom.toFloat() + ) + + } + + private fun setFallbackLayout() { + paddingLeft = 20.dpToPx() + paddingTop = 5.dpToPx() + paddingRight = 20.dpToPx() + paddingBottom = 5.dpToPx() + visibleWidth = if (doublePage) { + viewWidth / 2 - paddingLeft - paddingRight + } else { + viewWidth - paddingLeft - paddingRight + } + //留1dp画最后一行下划线 + visibleHeight = viewHeight - paddingTop - paddingBottom + visibleRight = viewWidth - paddingRight + visibleBottom = paddingTop + visibleHeight + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt new file mode 100755 index 000000000..6e1908873 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/LayoutProgressListener.kt @@ -0,0 +1,22 @@ +package io.legado.app.ui.book.read.page.provider + +import io.legado.app.ui.book.read.page.entities.TextPage + +interface LayoutProgressListener { + + /** + * 单页排版完成 + */ + fun onLayoutPageCompleted(index: Int, page: TextPage) {} + + /** + * 全部排版完成 + */ + fun onLayoutCompleted() {} + + /** + * 排版出现异常 + */ + fun onLayoutException(e: Throwable) {} + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt new file mode 100755 index 000000000..1cfb3ec4e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt @@ -0,0 +1,834 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Paint +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern +import io.legado.app.constant.PageAnim +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.BookContent +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.getBookSource +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.entities.TextLine +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.ui.book.read.page.entities.column.ImageColumn +import io.legado.app.ui.book.read.page.entities.column.TextColumn +import io.legado.app.utils.dpToPx +import io.legado.app.utils.fastSum +import io.legado.app.utils.getTextWidthsCompat +import io.legado.app.utils.splitNotBlank +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import java.util.LinkedList +import kotlin.math.roundToInt + +class TextChapterLayout( + scope: CoroutineScope, + private val textChapter: TextChapter, + private val textPages: ArrayList, + private val book: Book, + private val bookContent: BookContent, +) { + + @Volatile + private var listener: LayoutProgressListener? = textChapter + + private val paddingLeft = ChapterProvider.paddingLeft + private val paddingTop = ChapterProvider.paddingTop + + private val titlePaint = ChapterProvider.titlePaint + private val titlePaintTextHeight = ChapterProvider.titlePaintTextHeight + private val titlePaintFontMetrics = ChapterProvider.titlePaintFontMetrics + + private val contentPaint = ChapterProvider.contentPaint + private val contentPaintTextHeight = ChapterProvider.contentPaintTextHeight + private val contentPaintFontMetrics = ChapterProvider.contentPaintFontMetrics + + private val titleTopSpacing = ChapterProvider.titleTopSpacing + private val titleBottomSpacing = ChapterProvider.titleBottomSpacing + private val lineSpacingExtra = ChapterProvider.lineSpacingExtra + private val paragraphSpacing = ChapterProvider.paragraphSpacing + + private val visibleHeight = ChapterProvider.visibleHeight + private val visibleWidth = ChapterProvider.visibleWidth + + private val viewWidth = ChapterProvider.viewWidth + private val doublePage = ChapterProvider.doublePage + private val indentCharWidth = ChapterProvider.indentCharWidth + private val stringBuilder = StringBuilder() + + private val paragraphIndent = ReadBookConfig.paragraphIndent + private val titleMode = ReadBookConfig.titleMode + private val useZhLayout = ReadBookConfig.useZhLayout + private val isMiddleTitle = ReadBookConfig.isMiddleTitle + private val textFullJustify = ReadBookConfig.textFullJustify + private val pageAnim = book.getPageAnim() + + private var pendingTextPage = TextPage() + + private val bookChapter inline get() = textChapter.chapter + private val displayTitle inline get() = textChapter.title + private val chaptersSize inline get() = textChapter.chaptersSize + + private var durY = 0f + private var absStartX = paddingLeft + private var floatArray = FloatArray(128) + + private var isCompleted = false + private val job: Coroutine<*> + + var exception: Throwable? = null + + var channel = Channel(Channel.UNLIMITED) + + + init { + job = Coroutine.async( + scope, + start = CoroutineStart.LAZY, + executeContext = IO + ) { + launch { + val bookSource = book.getBookSource() ?: return@launch + BookHelp.saveImages(bookSource, book, bookChapter, bookContent.toString()) + } + getTextChapter(book, bookChapter, displayTitle, bookContent) + }.onError { + exception = it + onException(it) + }.onCancel { + channel.cancel() + }.onFinally { + isCompleted = true + } + job.start() + } + + fun setProgressListener(l: LayoutProgressListener?) { + try { + if (isCompleted) { + // no op + } else if (exception != null) { + l?.onLayoutException(exception!!) + } else { + listener = l + } + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } + } + + fun cancel() { + job.cancel() + listener = null + } + + private fun onPageCompleted() { + val textPage = pendingTextPage + textPage.index = textPages.size + textPage.chapterIndex = bookChapter.index + textPage.chapterSize = chaptersSize + textPage.title = displayTitle + textPage.doublePage = doublePage + textPage.paddingTop = paddingTop + textPage.isCompleted = true + textPage.textChapter = textChapter + textPage.upLinesPosition() + textPage.upRenderHeight() + textPages.add(textPage) + channel.trySend(textPage) + try { + listener?.onLayoutPageCompleted(textPages.lastIndex, textPage) + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } + } + + private fun onCompleted() { + channel.close() + try { + listener?.onLayoutCompleted() + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } finally { + listener = null + } + } + + private fun onException(e: Throwable) { + channel.close(e) + if (e is CancellationException) { + listener = null + return + } + try { + listener?.onLayoutException(e) + } catch (e: Exception) { + e.printStackTrace() + AppLog.put("调用布局进度监听回调出错\n${e.localizedMessage}", e) + } finally { + listener = null + } + } + + /** + * 获取拆分完的章节数据 + */ + private suspend fun getTextChapter( + book: Book, + bookChapter: BookChapter, + displayTitle: String, + bookContent: BookContent, + ) { + val contents = bookContent.textList + val imageStyle = book.getImageStyle() + val isSingleImageStyle = imageStyle.equals(Book.imgStyleSingle, true) + val isTextImageStyle = imageStyle.equals(Book.imgStyleText, true) + + if (titleMode != 2 || bookChapter.isVolume || contents.isEmpty()) { + //标题非隐藏 + displayTitle.splitNotBlank("\n").forEach { text -> + setTypeText( + book, + if (AppConfig.enableReview) text + ChapterProvider.reviewChar else text, + titlePaint, + titlePaintTextHeight, + titlePaintFontMetrics, + imageStyle, + isTitle = true, + emptyContent = contents.isEmpty(), + isVolumeTitle = bookChapter.isVolume + ) + pendingTextPage.lines.last().isParagraphEnd = true + stringBuilder.append("\n") + } + durY += titleBottomSpacing + + // 如果是单图模式且当前页有内容,强制分页 + if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) { + prepareNextPageIfNeed() + } + } + + val sb = StringBuffer() + var isSetTypedImage = false + contents.forEach { content -> + currentCoroutineContext().ensureActive() + if (isTextImageStyle) { + //图片样式为文字嵌入类型 + var text = content.replace(ChapterProvider.srcReplaceChar, "▣") + val srcList = LinkedList() + sb.setLength(0) + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { src -> + srcList.add(src) + matcher.appendReplacement(sb, ChapterProvider.srcReplaceChar) + } + } + matcher.appendTail(sb) + text = sb.toString() + setTypeText( + book, + text, + contentPaint, + contentPaintTextHeight, + contentPaintFontMetrics, + imageStyle, + srcList = srcList + ) + } else { + if (isSingleImageStyle && isSetTypedImage) { + isSetTypedImage = false + prepareNextPageIfNeed() + } + var start = 0 + if (content.contains(" 0 && size.height > 0) { + prepareNextPageIfNeed(durY) + var height = size.height + var width = size.width + when (imageStyle?.uppercase()) { + Book.imgStyleFull -> { + width = visibleWidth + height = size.height * visibleWidth / size.width + if (pageAnim != PageAnim.scrollPageAnim && height > visibleHeight - durY) { + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + prepareNextPageIfNeed(durY + height) + } + } + + Book.imgStyleSingle -> { + width = visibleWidth + height = size.height * visibleWidth / size.width + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + if (durY > 0f) { + prepareNextPageIfNeed() + } + + // 图片竖直方向居中:调整 Y 坐标 + if (height < visibleHeight) { + val adjustHeight = (visibleHeight - height) / 2f + durY = adjustHeight // 将 Y 坐标设置为居中位置 + } + } + + else -> { + if (size.width > visibleWidth) { + height = size.height * visibleWidth / size.width + width = visibleWidth + } + if (height > visibleHeight) { + width = width * visibleHeight / height + height = visibleHeight + } + prepareNextPageIfNeed(durY + height) + } + } + val textLine = TextLine(isImage = true) + textLine.text = " " + textLine.lineTop = durY + paddingTop + durY += height + textLine.lineBottom = durY + paddingTop + val (start, end) = if (visibleWidth > width) { + val adjustWidth = (visibleWidth - width) / 2f + Pair(adjustWidth, adjustWidth + width) + } else { + Pair(0f, width.toFloat()) + } + textLine.addColumn( + ImageColumn(start = absStartX + start, end = absStartX + end, src = src) + ) + calcTextLinePosition(textPages, textLine, stringBuilder.length) + stringBuilder.append(" ") // 确保翻页时索引计算正确 + pendingTextPage.addLine(textLine) + } + durY += textHeight * paragraphSpacing / 10f + } + + /** + * 排版文字 + */ + @Suppress("DEPRECATION") + private suspend fun setTypeText( + book: Book, + text: String, + textPaint: TextPaint, + textHeight: Float, + fontMetrics: Paint.FontMetrics, + imageStyle: String?, + isTitle: Boolean = false, + isFirstLine: Boolean = true, + emptyContent: Boolean = false, + isVolumeTitle: Boolean = false, + srcList: LinkedList? = null + ) { + val widthsArray = allocateFloatArray(text.length) + textPaint.getTextWidthsCompat(text, widthsArray) + val layout = if (useZhLayout) { + val (words, widths) = measureTextSplit(text, widthsArray) + val indentSize = if (isFirstLine) paragraphIndent.length else 0 + ZhLayout(text, textPaint, visibleWidth, words, widths, indentSize) + } else { + StaticLayout(text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true) + } + durY = when { + //标题y轴居中 + emptyContent && textPages.isEmpty() -> { + val textPage = pendingTextPage + if (textPage.lineSize == 0) { + val ty = (visibleHeight - layout.lineCount * textHeight) / 2 + if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat() + } else { + var textLayoutHeight = layout.lineCount * textHeight + val fistLine = textPage.getLine(0) + if (fistLine.lineTop < textLayoutHeight + titleTopSpacing) { + textLayoutHeight = fistLine.lineTop - titleTopSpacing + } + textPage.lines.forEach { + it.lineTop -= textLayoutHeight + it.lineBase -= textLayoutHeight + it.lineBottom -= textLayoutHeight + } + durY - textLayoutHeight + } + } + + isTitle && textPages.isEmpty() && pendingTextPage.lines.isEmpty() -> { + when (imageStyle?.uppercase()) { + Book.imgStyleSingle -> { + val ty = (visibleHeight - layout.lineCount * textHeight) / 2 + if (ty > titleTopSpacing) ty else titleTopSpacing.toFloat() + } + + else -> durY + titleTopSpacing + } + } + + else -> durY + } + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine(isTitle = isTitle) + prepareNextPageIfNeed(durY + textHeight) + val lineStart = layout.getLineStart(lineIndex) + val lineEnd = layout.getLineEnd(lineIndex) + val lineText = text.substring(lineStart, lineEnd) + val (words, widths) = measureTextSplit(lineText, widthsArray, lineStart) + val desiredWidth = widths.fastSum() + textLine.text = lineText + when { + lineIndex == 0 && layout.lineCount > 1 && !isTitle && isFirstLine -> { + //多行的第一行 非标题 + addCharsToLineFirst( + book, absStartX, textLine, words, textPaint, + desiredWidth, widths, srcList + ) + } + + lineIndex == layout.lineCount - 1 -> { + //最后一行、单行 + //标题x轴居中 + val startX = if ( + isTitle && + (isMiddleTitle || emptyContent || isVolumeTitle + || imageStyle?.uppercase() == Book.imgStyleSingle) + ) { + (visibleWidth - desiredWidth) / 2 + } else { + 0f + } + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, !isTitle && lineIndex == 0, widths, srcList + ) + } + + else -> { + if ( + isTitle && + (isMiddleTitle || emptyContent || isVolumeTitle + || imageStyle?.uppercase() == Book.imgStyleSingle) + ) { + //标题居中 + val startX = (visibleWidth - desiredWidth) / 2 + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, false, widths, srcList + ) + } else { + //中间行 + addCharsToLineMiddle( + book, absStartX, textLine, words, textPaint, + desiredWidth, 0f, widths, srcList + ) + } + } + } + if (doublePage) { + textLine.isLeftLine = absStartX < viewWidth / 2 + } + calcTextLinePosition(textPages, textLine, stringBuilder.length) + stringBuilder.append(lineText) + textLine.upTopBottom(durY, textHeight, fontMetrics) + val textPage = pendingTextPage + textPage.addLine(textLine) + durY += textHeight * lineSpacingExtra + if (textPage.height < durY) { + textPage.height = durY + } + } + durY += textHeight * paragraphSpacing / 10f + } + + private fun calcTextLinePosition( + textPages: ArrayList, + textLine: TextLine, + sbLength: Int + ) { + val lastLine = pendingTextPage.lines.lastOrNull { it.paragraphNum > 0 } + ?: textPages.lastOrNull()?.lines?.lastOrNull { it.paragraphNum > 0 } + val paragraphNum = when { + lastLine == null -> 1 + lastLine.isParagraphEnd -> lastLine.paragraphNum + 1 + else -> lastLine.paragraphNum + } + textLine.paragraphNum = paragraphNum + textLine.chapterPosition = + (textPages.lastOrNull()?.lines?.lastOrNull()?.run { + chapterPosition + charSize + if (isParagraphEnd) 1 else 0 + } ?: 0) + sbLength + textLine.pagePosition = sbLength + } + + /** + * 有缩进,两端对齐 + */ + private suspend fun addCharsToLineFirst( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + textPaint: TextPaint, + /**自然排版长度**/ + desiredWidth: Float, + textWidths: List, + srcList: LinkedList? + ) { + var x = 0f + if (!textFullJustify) { + addCharsToLineNatural( + book, absStartX, textLine, words, + x, true, textWidths, srcList + ) + return + } + val bodyIndent = paragraphIndent + repeat(bodyIndent.length) { + val x1 = x + indentCharWidth + textLine.addColumn( + TextColumn( + charData = ChapterProvider.indentChar, + start = absStartX + x, + end = absStartX + x1 + ) + ) + x = x1 + textLine.indentWidth = x + } + textLine.indentSize = bodyIndent.length + if (words.size > bodyIndent.length) { + val text1 = words.subList(bodyIndent.length, words.size) + val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size) + addCharsToLineMiddle( + book, absStartX, textLine, text1, textPaint, + desiredWidth, x, textWidths1, srcList + ) + } + } + + /** + * 无缩进,两端对齐 + */ + private suspend fun addCharsToLineMiddle( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + textPaint: TextPaint, + /**自然排版长度**/ + desiredWidth: Float, + /**起始x坐标**/ + startX: Float, + textWidths: List, + srcList: LinkedList? + ) { + if (!textFullJustify) { + addCharsToLineNatural( + book, absStartX, textLine, words, + startX, false, textWidths, srcList + ) + return + } + val residualWidth = visibleWidth - desiredWidth + val spaceSize = words.count { it == " " } + textLine.startX = absStartX + startX + if (spaceSize > 1) { + val d = residualWidth / spaceSize + textLine.wordSpacing = d + var x = startX + for (index in words.indices) { + val char = words[index] + val cw = textWidths[index] + val x1 = if (char == " ") { + if (index != words.lastIndex) (x + cw + d) else (x + cw) + } else { + (x + cw) + } + addCharToLine( + book, absStartX, textLine, char, + x, x1, index + 1 == words.size, srcList + ) + x = x1 + } + } else { + val gapCount: Int = words.lastIndex + val d = if (gapCount > 0) residualWidth / gapCount else 0f + textLine.extraLetterSpacingOffsetX = -d / 2 + textLine.extraLetterSpacing = d / textPaint.textSize + var x = startX + for (index in words.indices) { + val char = words[index] + val cw = textWidths[index] + val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) + addCharToLine( + book, absStartX, textLine, char, + x, x1, index + 1 == words.size, srcList + ) + x = x1 + } + } + exceed(absStartX, textLine, words) + } + + /** + * 自然排列 + */ + private suspend fun addCharsToLineNatural( + book: Book, + absStartX: Int, + textLine: TextLine, + words: List, + startX: Float, + hasIndent: Boolean, + textWidths: List, + srcList: LinkedList? + ) { + val indentLength = paragraphIndent.length + var x = startX + textLine.startX = absStartX + startX + for (index in words.indices) { + val char = words[index] + val cw = textWidths[index] + val x1 = x + cw + addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList) + x = x1 + if (hasIndent && index == indentLength - 1) { + textLine.indentWidth = x + } + } + exceed(absStartX, textLine, words) + } + + /** + * 添加字符 + */ + private suspend fun addCharToLine( + book: Book, + absStartX: Int, + textLine: TextLine, + char: String, + xStart: Float, + xEnd: Float, + isLineEnd: Boolean, + srcList: LinkedList? + ) { + val column = when { + srcList != null && char == ChapterProvider.srcReplaceChar -> { + val src = srcList.removeFirst() + ImageProvider.cacheImage(book, src, ReadBook.bookSource) + ImageColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + src = src + ) + } + +// isLineEnd && char == ChapterProvider.reviewChar -> { +// ReviewColumn( +// start = absStartX + xStart, +// end = absStartX + xEnd, +// count = 100 +// ) +// } + + else -> { + TextColumn( + start = absStartX + xStart, + end = absStartX + xEnd, + charData = char + ) + } + } + textLine.addColumn(column) + } + + /** + * 超出边界处理 + */ + private fun exceed(absStartX: Int, textLine: TextLine, words: List) { + var size = words.size + if (size < 2) return + val visibleEnd = absStartX + visibleWidth + val columns = textLine.columns + var offset = 0 + val endColumn = if (words.last() == " ") { + size-- + offset++ + columns[columns.lastIndex - 1] + } else { + columns.last() + } + val endX = endColumn.end.roundToInt() + if (endX > visibleEnd) { + textLine.exceed = true + val cc = (endX - visibleEnd) / size + for (i in 0.. visibleHeight || requestHeight == -1f) { + val textPage = pendingTextPage + // 双页的 durY 不正确,可能会小于实际高度 + if (textPage.height < durY) { + textPage.height = durY + } + if (doublePage && absStartX < viewWidth / 2) { + //当前页面左列结束 + textPage.leftLineSize = textPage.lineSize + absStartX = viewWidth / 2 + paddingLeft + } else { + //当前页面结束,设置各种值 + if (textPage.leftLineSize == 0) { + textPage.leftLineSize = textPage.lineSize + } + textPage.text = stringBuilder.toString() + currentCoroutineContext().ensureActive() + onPageCompleted() + //新建页面 + pendingTextPage = TextPage() + stringBuilder.clear() + absStartX = paddingLeft + } + durY = 0f + } + } + + private fun allocateFloatArray(size: Int): FloatArray { + if (size > floatArray.size) { + floatArray = FloatArray(size) + } + return floatArray + } + + private fun measureTextSplit( + text: String, + widthsArray: FloatArray, + start: Int = 0 + ): Pair, ArrayList> { + val length = text.length + var clusterCount = 0 + for (i in start.. 0) clusterCount++ + } + val widths = ArrayList(clusterCount) + val stringList = ArrayList(clusterCount) + var i = 0 + while (i < length) { + val clusterBaseIndex = i++ + widths.add(widthsArray[start + clusterBaseIndex]) + while (i < length && widthsArray[start + i] == 0f && !isZeroWidthChar(text[i])) { + i++ + } + stringList.add(text.substring(clusterBaseIndex, i)) + } + return stringList to widths + } + + private fun isZeroWidthChar(char: Char): Boolean { + val code = char.code + return code == 8203 || code == 8204 || code == 8205 || code == 8288 + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt new file mode 100755 index 000000000..4c8e51e9b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextMeasure.kt @@ -0,0 +1,143 @@ +package io.legado.app.ui.book.read.page.provider + +import android.text.TextPaint +import android.util.SparseArray +import androidx.core.util.getOrDefault +import kotlin.math.ceil + +class TextMeasure(private var paint: TextPaint) { + + private var chineseCommonWidth = paint.measureText("一") + private val asciiWidths = FloatArray(128) { -1f } + private val codePointWidths = SparseArray() + + private fun measureCodePoint(codePoint: Int): Float { + if (codePoint < 128) { + return asciiWidths[codePoint] + } + // 中文 Unicode 范围 U+4E00 - U+9FA5 + if (codePoint in 19968 .. 40869) { + return chineseCommonWidth + } + return codePointWidths.getOrDefault(codePoint, -1f) + } + + private fun measureCodePoints(codePoints: List) { + val charArray = String(codePoints.toIntArray(), 0, codePoints.size).toCharArray() + val widths = FloatArray(charArray.size) + paint.getTextWidths(charArray, 0, charArray.size, widths) + val widthsList = ArrayList(charArray.size) + val buf = IntArray(1) + for (i in charArray.indices) { + if (charArray[i].isLowSurrogate()) continue + val width = ceil(widths[i]) + widthsList.add(width) + // 可能需要检查是否不可见字符 + if (width == 0f && widthsList.size > 1) { + val lastIndex = widthsList.lastIndex + buf[0] = codePoints[lastIndex - 1] + widthsList[lastIndex - 1] = paint.measureText(String(buf, 0, 1)) + buf[0] = codePoints[lastIndex] + widthsList[lastIndex] = paint.measureText(String(buf, 0, 1)) + } + } + for (i in codePoints.indices) { + val codePoint = codePoints[i] + val width = widthsList[i] + if (codePoint < 128) { + asciiWidths[codePoint] = width + } else { + codePointWidths[codePoint] = width + } + } + } + + + fun measureTextSplit(text: String): Pair, ArrayList> { + var needMeasureCodePoints: HashSet? = null + val codePoints = text.toCodePoints() + val size = codePoints.size + val widths = ArrayList(size) + val stringList = ArrayList(size) + val buf = IntArray(1) + for (i in codePoints.indices) { + val codePoint = codePoints[i] + val width = measureCodePoint(codePoint) + widths.add(width) + if (width == -1f) { + if (needMeasureCodePoints == null) { + needMeasureCodePoints = hashSetOf() + } + needMeasureCodePoints.add(codePoint) + } + buf[0] = codePoint + stringList.add(String(buf, 0, 1)) + } + if (!needMeasureCodePoints.isNullOrEmpty()) { + measureCodePoints(needMeasureCodePoints.toList()) + for (i in codePoints.indices) { + if (widths[i] == -1f) { + widths[i] = measureCodePoint(codePoints[i]) + } + } + } + return stringList to widths + } + + fun measureText(text: String): Float { + var textWidth = 0f + var needMeasureCodePoints: ArrayList? = null + val codePoints = text.toCodePoints() + for (i in codePoints.indices) { + val codePoint = codePoints[i] + val width = measureCodePoint(codePoint) + if (width == -1f) { + if (needMeasureCodePoints == null) { + needMeasureCodePoints = ArrayList() + } + needMeasureCodePoints.add(codePoint) + continue + } + textWidth += width + } + if (!needMeasureCodePoints.isNullOrEmpty()) { + measureCodePoints(needMeasureCodePoints.toHashSet().toList()) + for (i in needMeasureCodePoints.indices) { + textWidth += measureCodePoint(needMeasureCodePoints[i]) + } + } + return textWidth + } + + private fun String.toCodePoints(): List { + val codePoints = ArrayList(length) + val charArray = toCharArray() + val size = length + var i = 0 + while (i < size) { + val c1 = charArray[i++] + var cp = c1.code + if (c1.isHighSurrogate() && i < size) { + val c2 = charArray[i] + if (c2.isLowSurrogate()) { + i++ + cp = Character.toCodePoint(c1, c2) + } + } + codePoints.add(cp) + } + return codePoints + } + + fun setPaint(paint: TextPaint) { + this.paint = paint + invalidate() + } + + private fun invalidate() { + chineseCommonWidth = paint.measureText("一") + codePointWidths.clear() + asciiWidths.fill(-1f) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt new file mode 100755 index 000000000..e261bcc02 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt @@ -0,0 +1,160 @@ +package io.legado.app.ui.book.read.page.provider + +import io.legado.app.R +import io.legado.app.model.ReadBook +import io.legado.app.ui.book.read.page.api.DataSource +import io.legado.app.ui.book.read.page.api.PageFactory +import io.legado.app.ui.book.read.page.entities.TextPage +import splitties.init.appCtx + +class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource) { + + private val keepSwipeTip = appCtx.getString(R.string.keep_swipe_tip) + + override fun hasPrev(): Boolean = with(dataSource) { + return hasPrevChapter() || pageIndex > 0 + } + + override fun hasNext(): Boolean = with(dataSource) { + return hasNextChapter() || (currentChapter != null && currentChapter?.isLastIndex(pageIndex) != true) + } + + override fun hasNextPlus(): Boolean = with(dataSource) { + return hasNextChapter() || pageIndex < (currentChapter?.pageSize ?: 1) - 2 + } + + override fun moveToFirst() { + ReadBook.setPageIndex(0) + } + + override fun moveToLast() = with(dataSource) { + currentChapter?.let { + if (it.pageSize == 0) { + ReadBook.setPageIndex(0) + } else { + ReadBook.setPageIndex(it.pageSize.minus(1)) + } + } ?: ReadBook.setPageIndex(0) + } + + override fun moveToNext(upContent: Boolean): Boolean = with(dataSource) { + return if (hasNext()) { + val pageIndex = pageIndex + if (currentChapter == null || currentChapter?.isLastIndex(pageIndex) == true) { + if ((currentChapter == null || isScroll) && nextChapter == null) { + return@with false + } + ReadBook.moveToNextChapter(upContent, false) + } else { + if (pageIndex < 0 || currentChapter?.isLastIndexCurrent(pageIndex) == true) { + return@with false + } + ReadBook.setPageIndex(pageIndex.plus(1)) + } + if (upContent) upContent(resetPageOffset = false) + true + } else + false + } + + override fun moveToPrev(upContent: Boolean): Boolean = with(dataSource) { + return if (hasPrev()) { + if (pageIndex <= 0) { + if (currentChapter == null && prevChapter == null) { + return@with false + } + if (prevChapter != null && prevChapter?.isCompleted == false) { + return@with false + } + ReadBook.moveToPrevChapter(upContent, upContentInPlace = false) + } else { + if (currentChapter == null) { + return@with false + } + ReadBook.setPageIndex(pageIndex.minus(1)) + } + if (upContent) upContent(resetPageOffset = false) + true + } else + false + } + + override val curPage: TextPage + get() = with(dataSource) { + ReadBook.msg?.let { + return@with TextPage(text = it).format() + } + currentChapter?.let { + return@with it.getPage(pageIndex) + ?: TextPage(title = it.title).apply { textChapter = it }.format() + } + return TextPage().format() + } + + override val nextPage: TextPage + get() = with(dataSource) { + ReadBook.msg?.let { + return@with TextPage(text = it).format() + } + currentChapter?.let { + val pageIndex = pageIndex + if (pageIndex < it.pageSize - 1) { + return@with it.getPage(pageIndex + 1)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + if (!it.isCompleted) { + return@with TextPage(title = it.title).format() + } + } + nextChapter?.let { + return@with it.getPage(0)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + return TextPage().format() + } + + override val prevPage: TextPage + get() = with(dataSource) { + ReadBook.msg?.let { + return@with TextPage(text = it).format() + } + currentChapter?.let { + val pageIndex = pageIndex + if (pageIndex > 0) { + return@with it.getPage(pageIndex - 1)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + if (!it.isCompleted) { + return@with TextPage(title = it.title).format() + } + } + prevChapter?.let { + return@with it.lastPage?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + return TextPage().format() + } + + override val nextPlusPage: TextPage + get() = with(dataSource) { + currentChapter?.let { + val pageIndex = pageIndex + if (pageIndex < it.pageSize - 2) { + return@with it.getPage(pageIndex + 2)?.removePageAloudSpan() + ?: TextPage(title = it.title).format() + } + if (!it.isCompleted) { + return@with TextPage(title = it.title).format() + } + nextChapter?.let { nc -> + if (pageIndex < it.pageSize - 1) { + return@with nc.getPage(0)?.removePageAloudSpan() + ?: TextPage(title = nc.title).format() + } + return@with nc.getPage(1)?.removePageAloudSpan() + ?: TextPage(text = keepSwipeTip).format() + } + } + return TextPage().format() + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt new file mode 100755 index 000000000..c84646d71 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ZhLayout.kt @@ -0,0 +1,278 @@ +package io.legado.app.ui.book.read.page.provider + +import android.graphics.Paint +import android.graphics.Rect +import android.os.Build +import android.text.Layout +import android.text.TextPaint +import java.util.WeakHashMap +import kotlin.math.max + +/** + * 针对中文的断行排版处理-by hoodie13 + * 因为StaticLayout对标点处理不符合国人习惯,继承Layout + * */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ZhLayout( + text: CharSequence, + textPaint: TextPaint, + width: Int, + words: List, + widths: List, + indentSize: Int +) : Layout(text, textPaint, width, Alignment.ALIGN_NORMAL, 0f, 0f) { + companion object { + private val postPanc = hashSetOf( + ",", "。", ":", "?", "!", "、", "”", "’", ")", "》", "}", + "】", ")", ">", "]", "}", ",", ".", "?", "!", ":", "」", ";", ";" + ) + private val prePanc = hashSetOf("“", "(", "《", "【", "‘", "‘", "(", "<", "[", "{", "「") + private val cnCharWidthCache = WeakHashMap() + } + + private val defaultCapacity = 10 + var lineStart = IntArray(defaultCapacity) + var lineWidth = FloatArray(defaultCapacity) + private var lineCount = 0 + private val curPaint = textPaint + private val cnCharWidth = cnCharWidthCache[textPaint] + ?: getDesiredWidth("我", textPaint).also { + cnCharWidthCache[textPaint] = it + } + + enum class BreakMod { NORMAL, BREAK_ONE_CHAR, BREAK_MORE_CHAR, CPS_1, CPS_2, CPS_3, } + class Locate { + var start: Float = 0f + var end: Float = 0f + } + + class Interval { + var total: Float = 0f + var single: Float = 0f + } + + init { + var line = 0 + var lineW = 0f + var cwPre = 0f + var length = 0 + words.forEachIndexed { index, s -> + val cw = widths[index] + var breakMod: BreakMod + var breakLine = false + lineW += cw + var offset = 0f + var breakCharCnt = 0 + + if (lineW > width) { + /*禁止在行尾的标点处理*/ + breakMod = if (index >= 1 && isPrePanc(words[index - 1])) { + if (index >= 2 && isPrePanc(words[index - 2])) BreakMod.CPS_2//如果后面还有一个禁首标点则异常 + else BreakMod.BREAK_ONE_CHAR //无异常场景 + } + /*禁止在行首的标点处理*/ + else if (isPostPanc(words[index])) { + if (index >= 1 && isPostPanc(words[index - 1])) BreakMod.CPS_1//如果后面还有一个禁首标点则异常,不过三个连续行尾标点的用法不通用 + else if (index >= 2 && isPrePanc(words[index - 2])) BreakMod.CPS_3//如果后面还有一个禁首标点则异常 + else BreakMod.BREAK_ONE_CHAR //无异常场景 + } else { + BreakMod.NORMAL //无异常场景 + } + + /*判断上述逻辑解决不了的特殊情况*/ + var reCheck = false + var breakIndex = 0 + if (breakMod == BreakMod.CPS_1 && + (inCompressible(widths[index]) || inCompressible(widths[index - 1])) + ) reCheck = true + if (breakMod == BreakMod.CPS_2 && + (inCompressible(widths[index - 1]) || inCompressible(widths[index - 2])) + ) reCheck = true + if (breakMod == BreakMod.CPS_3 && + (inCompressible(widths[index]) || inCompressible(widths[index - 2])) + ) reCheck = true + if (breakMod > BreakMod.BREAK_MORE_CHAR + && index < words.lastIndex && isPostPanc(words[index + 1]) + ) reCheck = true + + /*特殊标点使用难保证显示效果,所以不考虑间隔,直接查找到能满足条件的分割字*/ + var breakLength = 0 + if (reCheck && index > 2) { + val startPos = if (line == 0) indentSize else getLineStart(line) + breakMod = BreakMod.NORMAL + for (i in (index) downTo 1 + startPos) { + if (i == index) { + breakIndex = 0 + cwPre = 0f + } else { + breakIndex++ + breakLength += words[i].length + cwPre += widths[i] + } + if (!isPostPanc(words[i]) && !isPrePanc(words[i - 1])) { + breakMod = BreakMod.BREAK_MORE_CHAR + break + } + } + } + + when (breakMod) { + BreakMod.NORMAL -> {//模式0 正常断行 + offset = cw + lineStart[line + 1] = length + breakCharCnt = 1 + } + + BreakMod.BREAK_ONE_CHAR -> {//模式1 当前行下移一个字 + offset = cw + cwPre + lineStart[line + 1] = length - words[index - 1].length + breakCharCnt = 2 + } + + BreakMod.BREAK_MORE_CHAR -> {//模式2 当前行下移多个字 + offset = cw + cwPre + lineStart[line + 1] = length - breakLength + breakCharCnt = breakIndex + 1 + } + + BreakMod.CPS_1 -> {//模式3 两个后置标点压缩 + offset = 0f + lineStart[line + 1] = length + s.length + breakCharCnt = 0 + } + + BreakMod.CPS_2 -> { //模式4 前置标点压缩+前置标点压缩+字 + offset = 0f + lineStart[line + 1] = length + s.length + breakCharCnt = 0 + } + + BreakMod.CPS_3 -> {//模式5 前置标点压缩+字+后置标点压缩 + offset = 0f + lineStart[line + 1] = length + s.length + breakCharCnt = 0 + } + } + breakLine = true + } + + /*当前行写满情况下的断行*/ + if (breakLine) { + lineWidth[line] = lineW - offset + lineW = offset + addLineArray(++line) + } + /*已到最后一个字符*/ + if ((words.lastIndex) == index) { + if (!breakLine) { + offset = 0f + lineStart[line + 1] = length + s.length + lineWidth[line] = lineW - offset + lineW = offset + addLineArray(++line) + } + /*写满断行、段落末尾、且需要下移字符,这种特殊情况下要额外多一行*/ + else if (breakCharCnt > 0) { + lineStart[line + 1] = lineStart[line] + breakCharCnt + lineWidth[line] = lineW + addLineArray(++line) + } + } + length += s.length + cwPre = cw + } + + lineCount = line + + } + + private fun addLineArray(line: Int) { + if (lineStart.size <= line + 1) { + lineStart = lineStart.copyOf(line + defaultCapacity) + lineWidth = lineWidth.copyOf(line + defaultCapacity) + } + } + + private fun isPostPanc(string: String): Boolean { + return postPanc.contains(string) + } + + private fun isPrePanc(string: String): Boolean { + return prePanc.contains(string) + } + + private fun inCompressible(width: Float): Boolean { + return width < cnCharWidth + } + + private val gap = (cnCharWidth / 12.75).toFloat() + private fun getPostPancOffset(string: String): Float { + val textRect = Rect() + curPaint.getTextBounds(string, 0, 1, textRect) + return max(textRect.left.toFloat() - gap, 0f) + } + + private fun getPrePancOffset(string: String): Float { + val textRect = Rect() + curPaint.getTextBounds(string, 0, 1, textRect) + val d = max(cnCharWidth - textRect.right.toFloat() - gap, 0f) + return cnCharWidth / 2 - d + } + + fun getDesiredWidth(string: String, paint: TextPaint): Float { + var width = paint.measureText(string) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + width += paint.letterSpacing * paint.textSize + } + return width + } + + override fun getLineCount(): Int { + return lineCount + } + + override fun getLineTop(line: Int): Int { + return 0 + } + + override fun getLineDescent(line: Int): Int { + return 0 + } + + override fun getLineStart(line: Int): Int { + return lineStart[line] + } + + override fun getParagraphDirection(line: Int): Int { + return 0 + } + + override fun getLineContainsTab(line: Int): Boolean { + return true + } + + override fun getLineDirections(line: Int): Directions? { + return null + } + + override fun getTopPadding(): Int { + return 0 + } + + override fun getBottomPadding(): Int { + return 0 + } + + override fun getLineWidth(line: Int): Float { + return lineWidth[line] + } + + override fun getEllipsisStart(line: Int): Int { + return 0 + } + + override fun getEllipsisCount(line: Int): Int { + return 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt new file mode 100755 index 000000000..91a312ddc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt @@ -0,0 +1,46 @@ +package io.legado.app.ui.book.search + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ItemFilletTextBinding + + +class BookAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getViewBinding(parent: ViewGroup): ItemFilletTextBinding { + return ItemFilletTextBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFilletTextBinding, + item: Book, + payloads: MutableList + ) { + binding.run { + textView.text = item.name + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFilletTextBinding) { + holder.itemView.apply { + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it) + } + } + } + } + + interface CallBack { + fun showBookInfo(book: Book) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt new file mode 100755 index 000000000..355e54022 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt @@ -0,0 +1,55 @@ +package io.legado.app.ui.book.search + +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.SearchKeyword +import io.legado.app.databinding.ItemFilletTextBinding +import io.legado.app.ui.widget.anima.explosion_field.ExplosionField +import splitties.views.onLongClick + +class HistoryKeyAdapter(activity: SearchActivity, val callBack: CallBack) : + RecyclerAdapter(activity) { + + private val explosionField = ExplosionField.attach2Window(activity) + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getViewBinding(parent: ViewGroup): ItemFilletTextBinding { + return ItemFilletTextBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFilletTextBinding, + item: SearchKeyword, + payloads: MutableList + ) { + binding.run { + textView.text = item.word + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFilletTextBinding) { + holder.itemView.apply { + setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + callBack.searchHistory(it.word) + } + } + onLongClick { + explosionField.explode(this, true) + getItemByLayoutPosition(holder.layoutPosition)?.let { + callBack.deleteHistory(it) + } + } + } + } + + interface CallBack { + fun searchHistory(key: String) + fun deleteHistory(searchKeyword: SearchKeyword) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt new file mode 100755 index 000000000..935d6d00f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -0,0 +1,549 @@ +package io.legado.app.ui.book.search + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View.GONE +import android.view.View.VISIBLE +import android.widget.TextView +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.flexbox.FlexboxLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.SearchKeyword +import io.legado.app.databinding.ActivityBookSearchBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.applyNavigationBarMargin +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.applyTint +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import kotlin.math.abs + +class SearchActivity : VMBaseActivity(), + BookAdapter.CallBack, + HistoryKeyAdapter.CallBack, + SearchScopeDialog.Callback, + SearchAdapter.CallBack { + + override val binding by viewBinding(ActivityBookSearchBinding::inflate) + override val viewModel by viewModels() + + private val adapter by lazy { SearchAdapter(this, this) } + private val bookAdapter by lazy { + BookAdapter(this, this).apply { + setHasStableIds(true) + } + } + private val historyKeyAdapter by lazy { + HistoryKeyAdapter(this, this).apply { + setHasStableIds(true) + } + } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var menu: Menu? = null + private var groups: List? = null + private var historyFlowJob: Job? = null + private var booksFlowJob: Job? = null + private var precisionSearchMenuItem: MenuItem? = null + private var isManualStopSearch = false + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.llInputHelp.setBackgroundColor(backgroundColor) + initRecyclerView() + initSearchView() + initOtherView() + initData() + receiptIntent(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + receiptIntent(intent) + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_search, menu) + this.menu = menu + precisionSearchMenuItem = menu.findItem(R.id.menu_precision_search) + precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.transaction { + menu.removeGroup(R.id.menu_group_1) + menu.removeGroup(R.id.menu_group_2) + var hasChecked = false + val searchScopeNames = viewModel.searchScope.displayNames + if (viewModel.searchScope.isSource()) { + menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, searchScopeNames.first()).apply { + isChecked = true + hasChecked = true + } + } + val allSourceMenu = + menu.add(R.id.menu_group_2, R.id.menu_1, Menu.NONE, getString(R.string.all_source)) + .apply { + if (searchScopeNames.isEmpty()) { + isChecked = true + hasChecked = true + } + } + groups?.forEach { + if (searchScopeNames.contains(it)) { + menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, it).apply { + isChecked = true + hasChecked = true + } + } else { + menu.add(R.id.menu_group_2, Menu.NONE, Menu.NONE, it) + } + } + if (!hasChecked) { + viewModel.searchScope.update("") + allSourceMenu.isChecked = true + } + menu.setGroupCheckable(R.id.menu_group_1, true, false) + menu.setGroupCheckable(R.id.menu_group_2, true, true) + } + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_precision_search -> { + putPrefBoolean( + PreferKey.precisionSearch, + !getPrefBoolean(PreferKey.precisionSearch) + ) + precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch) + searchView.query?.toString()?.trim()?.let { + searchView.setQuery(it, true) + } + } + + R.id.menu_search_scope -> alertSearchScope() + R.id.menu_source_manage -> startActivity() + R.id.menu_log -> showDialogFragment(AppLogDialog()) + R.id.menu_1 -> viewModel.searchScope.update("") + else -> { + if (item.groupId == R.id.menu_group_1) { + viewModel.searchScope.remove(item.title.toString()) + } else if (item.groupId == R.id.menu_group_2) { + viewModel.searchScope.update(item.title.toString()) + } + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search_book_key) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + searchView.clearFocus() + query.trim().let { searchKey -> + isManualStopSearch = false + viewModel.saveSearchKey(searchKey) + viewModel.searchKey = "" + viewModel.search(searchKey) + } + visibleInputHelp(false) + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + viewModel.stop() + binding.fbStartStop.invisible() + upHistory(newText.trim()) + return false + } + }) + searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> + if (binding.refreshProgressBar.isAutoLoading || (!hasFocus && adapter.isNotEmpty() && searchView.query.isNotBlank())) { + visibleInputHelp(false) + } else { + visibleInputHelp(true) + } + } + visibleInputHelp(true) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.rvBookshelfSearch.setEdgeEffectColor(primaryColor) + binding.rvHistoryKey.setEdgeEffectColor(primaryColor) + binding.rvBookshelfSearch.layoutManager = FlexboxLayoutManager(this) + binding.rvBookshelfSearch.adapter = bookAdapter + binding.rvBookshelfSearch.applyNavigationBarMargin() + binding.rvHistoryKey.layoutManager = FlexboxLayoutManager(this) + binding.rvHistoryKey.adapter = historyKeyAdapter + binding.rvHistoryKey.applyNavigationBarMargin() + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.recyclerView.itemAnimator = null + binding.recyclerView.applyNavigationBarPadding() + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + if (positionStart == 0) { + binding.recyclerView.scrollToPosition(0) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + super.onItemRangeMoved(fromPosition, toPosition, itemCount) + if (toPosition == 0) { + binding.recyclerView.scrollToPosition(0) + } + } + }) + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (!recyclerView.canScrollVertically(1)) { + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition() + if (lastPosition == RecyclerView.NO_POSITION) { + return + } + val lastView = layoutManager.findViewByPosition(lastPosition) + if (lastView == null) { + scrollToBottom() + return + } + val bottom = + abs(lastView.bottom - recyclerView.height) - recyclerView.paddingBottom + if (bottom <= 1) { + scrollToBottom() + } + } + } + }) + } + + private fun initOtherView() { + binding.fbStartStop.backgroundTintList = + Selector.colorBuild() + .setDefaultColor(accentColor) + .setPressedColor(ColorUtils.darkenColor(accentColor)) + .create() + binding.fbStartStop.setOnClickListener { + if (viewModel.isSearchLiveData.value == true) { + isManualStopSearch = true + viewModel.stop() + binding.refreshProgressBar.isAutoLoading = false + } else { + viewModel.search("") + } + } + binding.fbStartStop.applyNavigationBarMargin(true) + binding.tvClearHistory.setOnClickListener { alertClearHistory() } + } + + private fun initData() { + viewModel.searchScope.stateLiveData.observe(this) { + if (!binding.llInputHelp.isVisible) { + searchView.query?.toString()?.trim()?.let { + searchView.setQuery(it, true) + } + } + } + viewModel.isSearchLiveData.observe(this) { + if (it) { + startSearch() + } else { + searchFinally() + } + } + viewModel.searchBookLiveData.observe(this) { + adapter.setItems(it) + } + lifecycleScope.launch { + appDb.bookSourceDao.flowEnabledGroups().collect { + groups = it + } + } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.resume() + try { + awaitCancellation() + } finally { + viewModel.pause() + } + } + } + } + + /** + * 处理传入数据 + */ + private fun receiptIntent(intent: Intent? = null) { + val searchScope = intent?.getStringExtra("searchScope") + searchScope?.let { + viewModel.searchScope.update(searchScope, false) + } + val key = intent?.getStringExtra("key") + if (key.isNullOrBlank()) { + searchView.findViewById(androidx.appcompat.R.id.search_src_text) + .requestFocus() + } else { + searchView.setQuery(key, true) + } + } + + /** + * 滚动到底部事件 + */ + private fun scrollToBottom() { + if (isManualStopSearch) { + return + } + if (viewModel.isSearchLiveData.value == false + && viewModel.searchKey.isNotEmpty() + && viewModel.hasMore + ) { + viewModel.search("") + } + } + + /** + * 打开关闭输入帮助 + */ + private fun visibleInputHelp(visible: Boolean) { + if (visible) { + upHistory(searchView.query.toString()) + binding.llInputHelp.visibility = VISIBLE + } else { + binding.llInputHelp.visibility = GONE + } + } + + /** + * 更新搜索历史 + */ + private fun upHistory(key: String? = null) { + booksFlowJob?.cancel() + booksFlowJob = lifecycleScope.launch { + if (key.isNullOrBlank()) { + binding.tvBookShow.gone() + binding.rvBookshelfSearch.gone() + } else { + appDb.bookDao.flowSearch(key).conflate().collect { + if (it.isEmpty()) { + binding.tvBookShow.gone() + binding.rvBookshelfSearch.gone() + } else { + binding.tvBookShow.visible() + binding.rvBookshelfSearch.visible() + } + bookAdapter.setItems(it) + } + } + } + historyFlowJob?.cancel() + historyFlowJob = lifecycleScope.launch { + when { + key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime() + else -> appDb.searchKeywordDao.flowSearch(key) + }.catch { + AppLog.put("搜索界面获取搜索历史数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { + historyKeyAdapter.setItems(it) + if (it.isEmpty()) { + binding.tvClearHistory.invisible() + } else { + binding.tvClearHistory.visible() + } + } + } + } + + /** + * 开始搜索 + */ + private fun startSearch() { + binding.refreshProgressBar.visible() + binding.refreshProgressBar.isAutoLoading = true + binding.fbStartStop.setImageResource(R.drawable.ic_stop_black_24dp) + binding.fbStartStop.visible() + } + + /** + * 搜索结束 + */ + private fun searchFinally() { + binding.refreshProgressBar.isAutoLoading = false + binding.refreshProgressBar.gone() + if (!isManualStopSearch && viewModel.hasMore) { + binding.fbStartStop.setImageResource(R.drawable.ic_play_24dp) + } else { + binding.fbStartStop.invisible() + } + } + + override fun observeLiveBus() { + viewModel.upAdapterLiveData.observe(this) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, bundleOf(it to null)) + } + viewModel.searchFinishLiveData.observe(this) { isEmpty -> + if (!isEmpty || viewModel.searchScope.isAll()) return@observe + alert("搜索结果为空") { + val precisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch) + val displayScope = viewModel.searchScope.display + if (precisionSearch) { + setMessage("${displayScope}分组搜索结果为空,是否关闭精准搜索?") + yesButton { + appCtx.putPrefBoolean(PreferKey.precisionSearch, false) + precisionSearchMenuItem?.isChecked = false + viewModel.searchKey = "" + viewModel.search(searchView.query.toString()) + } + } else { + setMessage("${displayScope}分组搜索结果为空,是否切换到全部分组?") + yesButton { + viewModel.searchScope.update("") + } + } + noButton() + } + } + } + + /** + * 显示书籍详情 + */ + override fun showBookInfo(name: String, author: String, bookUrl: String) { + startActivity { + putExtra("name", name) + putExtra("author", author) + putExtra("bookUrl", bookUrl) + } + } + + /** + * 是否已经加入书架 + */ + override fun isInBookshelf(book: SearchBook): Boolean { + return viewModel.isInBookShelf(book) + } + + /** + * 显示书籍详情 + */ + override fun showBookInfo(book: Book) { + showBookInfo(book.name, book.author, book.bookUrl) + } + + /** + * 点击历史关键字 + */ + override fun searchHistory(key: String) { + lifecycleScope.launch { + when { + searchView.query.toString() == key -> { + searchView.setQuery(key, true) + } + + withContext(IO) { appDb.bookDao.findByName(key).isEmpty() } -> { + searchView.setQuery(key, true) + } + + else -> { + searchView.setQuery(key, false) + } + } + } + } + + /** + * 删除搜索记录 + */ + override fun deleteHistory(searchKeyword: SearchKeyword) { + viewModel.deleteHistory(searchKeyword) + } + + + override fun onSearchScopeOk(searchScope: SearchScope) { + viewModel.searchScope.update(searchScope.toString()) + } + + private fun alertSearchScope() { + showDialogFragment() + } + + private fun alertClearHistory() { + alert(R.string.draw) { + setMessage(R.string.sure_clear_search_history) + yesButton { + viewModel.clearHistory() + } + noButton() + } + } + + override fun finish() { + if (searchView.hasFocus()) { + searchView.clearFocus() + return + } + super.finish() + } + + companion object { + + fun start(context: Context, key: String?) { + context.startActivity { + putExtra("key", key) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt new file mode 100755 index 000000000..34b15d817 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt @@ -0,0 +1,155 @@ +package io.legado.app.ui.book.search + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.SearchBook +import io.legado.app.databinding.ItemSearchBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class SearchAdapter(context: Context, val callBack: CallBack) : + DiffRecyclerAdapter(context) { + + override val keepScrollPosition = true + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return when { + oldItem.name != newItem.name -> false + oldItem.author != newItem.author -> false + else -> true + } + } + + override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { + return false + } + + override fun getChangePayload(oldItem: SearchBook, newItem: SearchBook): Any { + val payload = Bundle() + payload.putInt("origins", newItem.origins.size) + if (oldItem.coverUrl != newItem.coverUrl) + payload.putString("cover", newItem.coverUrl) + if (oldItem.kind != newItem.kind) + payload.putString("kind", newItem.kind) + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) + payload.putString("last", newItem.latestChapterTitle) + if (oldItem.intro != newItem.intro) + payload.putString("intro", newItem.intro) + return payload + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemSearchBinding { + return ItemSearchBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSearchBinding, + item: SearchBook, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + bind(binding, item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bindChange(binding, item, bundle) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSearchBinding) { + binding.root.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it.name, it.author, it.bookUrl) + } + } + } + + private fun bind(binding: ItemSearchBinding, searchBook: SearchBook) { + binding.run { + tvName.text = searchBook.name + tvAuthor.text = context.getString(R.string.author_show, searchBook.author) + ivInBookshelf.isVisible = callBack.isInBookshelf(searchBook) + bvOriginCount.setBadgeCount(searchBook.origins.size) + upLasted(binding, searchBook.latestChapterTitle) + tvIntroduce.text = searchBook.trimIntro(context) + upKind(binding, searchBook.getKindList()) + ivCover.load( + searchBook.coverUrl, + searchBook.name, + searchBook.author, + AppConfig.loadCoverOnlyWifi, + searchBook.origin + ) + } + } + + private fun bindChange(binding: ItemSearchBinding, searchBook: SearchBook, bundle: Bundle) { + binding.run { + bundle.keySet().forEach { + when (it) { + "origins" -> bvOriginCount.setBadgeCount(searchBook.origins.size) + "last" -> upLasted(binding, searchBook.latestChapterTitle) + "intro" -> tvIntroduce.text = searchBook.trimIntro(context) + "kind" -> upKind(binding, searchBook.getKindList()) + "isInBookshelf" -> ivInBookshelf.isVisible = callBack.isInBookshelf(searchBook) + "cover" -> ivCover.load( + searchBook.coverUrl, + searchBook.name, + searchBook.author, + false, + searchBook.origin + ) + } + } + } + } + + private fun upLasted(binding: ItemSearchBinding, latestChapterTitle: String?) { + binding.run { + if (latestChapterTitle.isNullOrEmpty()) { + tvLasted.gone() + } else { + tvLasted.text = + context.getString(R.string.lasted_show, latestChapterTitle) + tvLasted.visible() + } + } + } + + private fun upKind(binding: ItemSearchBinding, kinds: List) = binding.run { + if (kinds.isEmpty()) { + llKind.gone() + } else { + llKind.visible() + llKind.setLabels(kinds) + } + } + + interface CallBack { + + /** + * 是否已经加入书架 + */ + fun isInBookshelf(book: SearchBook): Boolean + + /** + * 显示书籍详情 + */ + fun showBookInfo(name: String, author: String, bookUrl: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt new file mode 100755 index 000000000..7490b62f6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt @@ -0,0 +1,153 @@ +package io.legado.app.ui.book.search + +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.splitNotBlank +import splitties.init.appCtx + +/** + * 搜索范围 + */ +@Suppress("unused") +data class SearchScope(private var scope: String) { + + constructor(groups: List) : this(groups.joinToString(",")) + + constructor(source: BookSource) : this( + "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" + ) + + constructor(source: BookSourcePart) : this( + "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" + ) + + override fun toString(): String { + return scope + } + + val stateLiveData = MutableLiveData(scope) + + fun update(scope: String, postValue: Boolean = true) { + this.scope = scope + if (postValue) stateLiveData.postValue(scope) + save() + } + + fun update(groups: List) { + scope = groups.joinToString(",") + stateLiveData.postValue(scope) + save() + } + + fun update(source: BookSource) { + scope = "${source.bookSourceName}::${source.bookSourceUrl}" + stateLiveData.postValue(scope) + save() + } + + fun isSource(): Boolean { + return scope.contains("::") + } + + val display: String + get() { + if (scope.contains("::")) { + return scope.substringBefore("::") + } + if (scope.isEmpty()) { + return appCtx.getString(R.string.all_source) + } + return scope + } + + /** + * 搜索范围显示 + */ + val displayNames: List + get() { + val list = arrayListOf() + if (scope.contains("::")) { + list.add(scope.substringBefore("::")) + } else { + scope.splitNotBlank(",").forEach { + list.add(it) + } + } + return list + } + + fun remove(scope: String) { + if (isSource()) { + this.scope = "" + } else { + val stringBuilder = StringBuilder() + this.scope.split(",").forEach { + if (it != scope) { + if (stringBuilder.isNotEmpty()) { + stringBuilder.append(",") + } + stringBuilder.append(it) + } + } + this.scope = stringBuilder.toString() + } + stateLiveData.postValue(this.scope) + } + + /** + * 搜索范围书源 + */ + fun getBookSourceParts(): List { + val list = hashSetOf() + if (scope.isEmpty()) { + list.addAll(appDb.bookSourceDao.allEnabledPart) + } else { + if (scope.contains("::")) { + scope.substringAfter("::").let { + appDb.bookSourceDao.getBookSourcePart(it)?.let { source -> + list.add(source) + } + } + } else { + val oldScope = scope.splitNotBlank(",") + val newScope = oldScope.filter { + val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it) + list.addAll(bookSources) + bookSources.isNotEmpty() + } + if (oldScope.size != newScope.size) { + update(newScope) + stateLiveData.postValue(scope) + } + } + if (list.isEmpty()) { + scope = "" + appDb.bookSourceDao.allEnabledPart.let { + if (it.isNotEmpty()) { + stateLiveData.postValue(scope) + list.addAll(it) + } + } + } + } + return list.sortedBy { it.customOrder } + } + + fun isAll(): Boolean { + return scope.isEmpty() + } + + fun save() { + AppConfig.searchScope = scope + if (isAll() || isSource() || scope.contains(",")) { + AppConfig.searchGroup = "" + } else { + AppConfig.searchGroup = scope + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt new file mode 100755 index 000000000..0857fcda9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt @@ -0,0 +1,255 @@ +package io.legado.app.ui.book.search + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.constant.AppLog +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.databinding.DialogSearchScopeBinding +import io.legado.app.databinding.ItemCheckBoxBinding +import io.legado.app.databinding.ItemRadioButtonBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.applyTint +import io.legado.app.utils.flowWithLifecycleAndDatabaseChange +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) { + + private val binding by viewBinding(DialogSearchScopeBinding::bind) + private var sourceFlowJob: Job? = null + val callback: Callback get() = parentFragment as? Callback ?: activity as Callback + var groups: List = emptyList() + val screenSources = arrayListOf() + var screenText: String? = null + + val adapter by lazy { + RecyclerAdapter() + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.8f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.recyclerView.adapter = adapter + initMenu() + initSearchView() + initOtherView() + initData() + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.book_search_scope) + binding.toolBar.menu.applyTint(requireContext()) + } + + private fun initSearchView() { + val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + screenText = newText + upData() + return false + } + + }) + } + + private fun initOtherView() { + binding.rgScope.setOnCheckedChangeListener { _, checkedId -> + binding.toolBar.menu.findItem(R.id.menu_screen)?.isVisible = checkedId == R.id.rb_source + upData() + } + binding.tvCancel.setOnClickListener { + dismiss() + } + binding.tvAllSource.setOnClickListener { + callback.onSearchScopeOk(SearchScope("")) + dismiss() + } + binding.tvOk.setOnClickListener { + if (binding.rbGroup.isChecked) { + callback.onSearchScopeOk(SearchScope(adapter.selectGroups)) + } else { + val selectSource = adapter.selectSource + if (selectSource != null) { + callback.onSearchScopeOk(SearchScope(selectSource)) + } else { + callback.onSearchScopeOk(SearchScope("")) + } + } + dismiss() + } + } + + private fun initData() { + lifecycleScope.launch { + groups = withContext(IO) { + appDb.bookSourceDao.allEnabledGroups() + } + upData() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun upData() { + if (binding.rbSource.isChecked) { + upBookSource(screenText) + } else { + adapter.notifyDataSetChanged() + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun upBookSource(searchKey: String? = null) { + sourceFlowJob?.cancel() + sourceFlowJob = lifecycleScope.launch { + when { + searchKey.isNullOrEmpty() -> { + appDb.bookSourceDao.flowAll() + } + + else -> { + appDb.bookSourceDao.flowSearch(searchKey) + } + }.flowWithLifecycleAndDatabaseChange( + lifecycle, + table = AppDatabase.BOOK_SOURCE_TABLE_NAME + ).catch { + AppLog.put("多分组/书源界面更新书源出错", it) + }.flowOn(IO).conflate().collect { data -> + screenSources.clear() + screenSources.addAll(data) + adapter.notifyDataSetChanged() + delay(500) + } + } + } + + inner class RecyclerAdapter : RecyclerView.Adapter() { + + val selectGroups = arrayListOf() + var selectSource: BookSourcePart? = null + + override fun getItemViewType(position: Int): Int { + return if (binding.rbSource.isChecked) { + 1 + } else { + 0 + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return if (viewType == 1) { + ItemViewHolder(ItemRadioButtonBinding.inflate(layoutInflater, parent, false)) + } else { + ItemViewHolder(ItemCheckBoxBinding.inflate(layoutInflater, parent, false)) + } + } + + override fun onBindViewHolder( + holder: ItemViewHolder, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + when (holder.binding) { + is ItemCheckBoxBinding -> { + groups.getOrNull(position)?.let { + holder.binding.checkBox.isChecked = selectGroups.contains(it) + holder.binding.checkBox.text = it + } + } + + is ItemRadioButtonBinding -> { + screenSources.getOrNull(position)?.let { + holder.binding.radioButton.isChecked = selectSource == it + holder.binding.radioButton.text = it.bookSourceName + } + } + } + } + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + when (holder.binding) { + is ItemCheckBoxBinding -> { + groups.getOrNull(position)?.let { + holder.binding.checkBox.isChecked = selectGroups.contains(it) + holder.binding.checkBox.text = it + holder.binding.checkBox.setOnUserCheckedChangeListener { isChecked -> + if (isChecked) { + selectGroups.add(it) + } else { + selectGroups.remove(it) + } + holder.itemView.post { + notifyItemRangeChanged(0, itemCount, "up") + } + } + } + } + + is ItemRadioButtonBinding -> { + screenSources.getOrNull(position)?.let { + holder.binding.radioButton.isChecked = selectSource == it + holder.binding.radioButton.text = it.bookSourceName + holder.binding.radioButton.setOnUserCheckedChangeListener { isChecked -> + if (isChecked) { + selectSource = it + } + holder.itemView.post { + notifyItemRangeChanged(0, itemCount, "up") + } + } + } + } + } + } + + override fun getItemCount(): Int { + return if (binding.rbSource.isChecked) { + screenSources.size + } else { + groups.size + } + } + + } + + interface Callback { + + /** + * 搜索范围确认 + */ + fun onSearchScopeOk(searchScope: SearchScope) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt new file mode 100755 index 000000000..53b91af12 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -0,0 +1,162 @@ +package io.legado.app.ui.book.search + +import android.app.Application +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.SearchKeyword +import io.legado.app.help.book.isNotShelf +import io.legado.app.help.config.AppConfig +import io.legado.app.model.webBook.SearchModel +import io.legado.app.utils.ConflateLiveData +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.mapLatest +import java.util.concurrent.ConcurrentHashMap + +@OptIn(ExperimentalCoroutinesApi::class) +class SearchViewModel(application: Application) : BaseViewModel(application) { + val handler = Handler(Looper.getMainLooper()) + val bookshelf: MutableSet = ConcurrentHashMap.newKeySet() + val upAdapterLiveData = MutableLiveData() + var searchBookLiveData = ConflateLiveData>(1000) + val searchScope: SearchScope = SearchScope(AppConfig.searchScope) + var searchFinishLiveData = MutableLiveData() + var isSearchLiveData = MutableLiveData() + var searchKey: String = "" + var hasMore = true + private var searchID = 0L + private val searchModel = SearchModel(viewModelScope, object : SearchModel.CallBack { + + override fun getSearchScope(): SearchScope { + return searchScope + } + + override fun onSearchStart() { + isSearchLiveData.postValue(true) + } + + override fun onSearchSuccess(searchBooks: List) { + searchBookLiveData.postValue(searchBooks) + } + + override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) { + this@SearchViewModel.hasMore = hasMore + isSearchLiveData.postValue(false) + searchFinishLiveData.postValue(isEmpty) + } + + override fun onSearchCancel(exception: Throwable?) { + isSearchLiveData.postValue(false) + exception?.let { + context.toastOnUi(it.localizedMessage) + } + } + + }) + + init { + execute { + appDb.bookDao.flowAll().mapLatest { books -> + val keys = arrayListOf() + books.filterNot { it.isNotShelf } + .forEach { + keys.add("${it.name}-${it.author}") + keys.add(it.name) + keys.add(it.bookUrl) + } + keys + }.catch { + AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it) + }.collect { + bookshelf.clear() + bookshelf.addAll(it) + upAdapterLiveData.postValue("isInBookshelf") + } + }.onError { + AppLog.put("加载书架数据失败", it) + } + } + + fun isInBookShelf(book: SearchBook): Boolean { + val name = book.name + val author = book.author + val bookUrl = book.bookUrl + val key = if (author.isNotBlank()) "$name-$author" else name + return bookshelf.contains(key) || bookshelf.contains(bookUrl) + } + + /** + * 开始搜索 + */ + fun search(key: String) { + execute { + if ((searchKey == key) || key.isNotEmpty()) { + searchModel.cancelSearch() + searchID = System.currentTimeMillis() + searchBookLiveData.postValue(emptyList()) + searchKey = key + hasMore = true + } + if (searchKey.isEmpty()) { + return@execute + } + searchModel.search(searchID, searchKey) + } + } + + /** + * 停止搜索 + */ + fun stop() { + searchModel.cancelSearch() + } + + fun pause() { + searchModel.pause() + } + + fun resume() { + searchModel.resume() + } + + /** + * 保存搜索关键字 + */ + fun saveSearchKey(key: String) { + execute { + appDb.searchKeywordDao.get(key)?.let { + it.usage += 1 + it.lastUseTime = System.currentTimeMillis() + appDb.searchKeywordDao.update(it) + } ?: appDb.searchKeywordDao.insert(SearchKeyword(key, 1)) + } + } + + /** + * 清楚搜索关键字 + */ + fun clearHistory() { + execute { + appDb.searchKeywordDao.deleteAll() + } + } + + fun deleteHistory(searchKeyword: SearchKeyword) { + execute { + appDb.searchKeywordDao.delete(searchKeyword) + } + } + + override fun onCleared() { + super.onCleared() + searchModel.close() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt new file mode 100755 index 000000000..0b902e9d2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt @@ -0,0 +1,258 @@ +package io.legado.app.ui.book.searchContent + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.EditText +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.core.view.allViews +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.ActivitySearchContentBinding +import io.legado.app.help.IntentData +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isLocal +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.widget.recycler.UpLinearLayoutManager +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.applyNavigationBarMargin +import io.legado.app.utils.applyTint +import io.legado.app.utils.invisible +import io.legado.app.utils.observeEvent +import io.legado.app.utils.postEvent +import io.legado.app.utils.showSoftInput +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class SearchContentActivity : + VMBaseActivity(), + SearchContentAdapter.Callback { + + override val binding by viewBinding(ActivitySearchContentBinding::inflate) + override val viewModel by viewModels() + private val adapter by lazy { SearchContentAdapter(this, this) } + private val mLayoutManager by lazy { UpLinearLayoutManager(this) } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var durChapterIndex = 0 + private var searchJob: Job? = null + private var initJob: Job? = null + + override fun onActivityCreated(savedInstanceState: Bundle?) { + val bbg = bottomBackground + val btc = getPrimaryTextColor(ColorUtils.isColorLight(bbg)) + binding.llSearchBaseInfo.setBackgroundColor(bbg) + binding.llSearchBaseInfo.applyNavigationBarMargin() + binding.tvCurrentSearchInfo.setTextColor(btc) + binding.ivSearchContentTop.setColorFilter(btc) + binding.ivSearchContentBottom.setColorFilter(btc) + val searchResultList = IntentData.get>("searchResultList") + val position = intent.getIntExtra("searchResultIndex", 0) + val noSearchResult = searchResultList == null + initSearchView(noSearchResult) + initRecyclerView() + initView() + val bookUrl = intent.getStringExtra("bookUrl") ?: return + viewModel.initBook(bookUrl) { + initSearchResultList(searchResultList, position) + initBook(noSearchResult) + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.content_search, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_enable_replace)?.isChecked = viewModel.replaceEnabled + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_enable_replace -> { + viewModel.replaceEnabled = !viewModel.replaceEnabled + item.isChecked = viewModel.replaceEnabled + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initSearchResultList(list: List?, position: Int) { + list ?: return + viewModel.searchResultList.addAll(list) + viewModel.searchResultCounts = list.size + adapter.setItems(list) + binding.recyclerView.scrollToPosition(position) + } + + private fun initSearchView(requestFocus: Boolean) { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search) + if (requestFocus) searchView.isIconified = false + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + startContentSearch(query.trim()) + searchView.clearFocus() + return false + } + + override fun onQueryTextChange(newText: String): Boolean { + return false + } + }) + } + + private fun initRecyclerView() { + binding.recyclerView.layoutManager = mLayoutManager + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter + } + + private fun initView() { + binding.ivSearchContentTop.setOnClickListener { + mLayoutManager.scrollToPositionWithOffset(0, 0) + } + binding.ivSearchContentBottom.setOnClickListener { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + binding.tvCurrentSearchInfo.setOnClickListener { + searchView.allViews.forEach { view -> + if (view is EditText) { + view.showSoftInput() + return@setOnClickListener + } + } + } + binding.fbStop.setOnClickListener { + searchJob?.cancel() + } + } + + @SuppressLint("SetTextI18n") + private fun initBook(submit: Boolean = true) { + binding.tvCurrentSearchInfo.text = + this.getString(R.string.search_content_size) + ": ${viewModel.searchResultCounts}" + viewModel.book?.let { + initCacheFileNames(it) + durChapterIndex = it.durChapterIndex + intent.getStringExtra("searchWord")?.let { searchWord -> + searchView.setQuery(searchWord, submit) + } + } + } + + private fun initCacheFileNames(book: Book) { + initJob = lifecycleScope.launch { + withContext(IO) { + viewModel.cacheChapterNames.addAll(BookHelp.getChapterFiles(book)) + } + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + } + + override fun observeLiveBus() { + observeEvent>(EventBus.SAVE_CONTENT) { (book, chapter) -> + viewModel.book?.bookUrl?.let { bookUrl -> + if (book.bookUrl == bookUrl) { + viewModel.cacheChapterNames.add(chapter.getFileName()) + adapter.notifyItemChanged(chapter.index, true) + } + } + } + } + + @SuppressLint("SetTextI18n") + fun startContentSearch(query: String) { + // 按章节搜索内容 + if (query.isBlank()) return + searchJob?.cancel() + adapter.clearItems() + viewModel.searchResultList.clear() + viewModel.searchResultCounts = 0 + viewModel.lastQuery = query + binding.refreshProgressBar.isAutoLoading = true + binding.fbStop.visible() + searchJob = lifecycleScope.launch(IO) { + initJob?.join() + kotlin.runCatching { + appDb.bookChapterDao.getChapterList(viewModel.bookUrl).forEach { bookChapter -> + ensureActive() + val searchResults = if (isLocalBook + || viewModel.cacheChapterNames.contains(bookChapter.getFileName()) + ) { + viewModel.searchChapter(query, bookChapter) + } else { + return@forEach + } + ensureActive() + if (searchResults.isNotEmpty()) { + viewModel.searchResultList.addAll(searchResults) + binding.tvCurrentSearchInfo.post { + binding.tvCurrentSearchInfo.text = + this@SearchContentActivity.getString(R.string.search_content_size) + ": ${viewModel.searchResultCounts}" + adapter.addItems(searchResults) + } + } + } + if (viewModel.searchResultCounts == 0) { + val noSearchResult = + SearchResult(resultText = getString(R.string.search_content_empty)) + binding.tvCurrentSearchInfo.post { + adapter.addItem(noSearchResult) + } + } + }.onFailure { + AppLog.put("全文搜索出错\n${it.localizedMessage}", it) + } + binding.tvCurrentSearchInfo.post { + binding.fbStop.invisible() + binding.refreshProgressBar.isAutoLoading = false + } + } + } + + private val isLocalBook: Boolean + get() = viewModel.book?.isLocal == true + + override fun openSearchResult(searchResult: SearchResult, index: Int) { + searchJob?.cancel() + postEvent(EventBus.SEARCH_RESULT, viewModel.searchResultList as List) + val searchData = Intent() + val key = System.currentTimeMillis() + IntentData.put("searchResult$key", searchResult) + IntentData.put("searchResultList$key", viewModel.searchResultList) + searchData.putExtra("key", key) + searchData.putExtra("index", index) + setResult(RESULT_OK, searchData) + finish() + } + + override fun durChapterIndex(): Int { + return durChapterIndex + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt new file mode 100755 index 000000000..b072291de --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.book.searchContent + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemSearchListBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.hexString + + +class SearchContentAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + val textColor = context.getCompatColor(R.color.primaryText).hexString.substring(2) + val accentColor = context.accentColor.hexString.substring(2) + + override fun getViewBinding(parent: ViewGroup): ItemSearchListBinding { + return ItemSearchListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemSearchListBinding, + item: SearchResult, + payloads: MutableList + ) { + binding.run { + val isDur = callback.durChapterIndex() == item.chapterIndex + if (payloads.isEmpty()) { + tvSearchResult.text = item.getHtmlCompat(textColor, accentColor) + tvSearchResult.paint.isFakeBoldText = isDur + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemSearchListBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (it.query.isNotBlank()) callback.openSearchResult(it, holder.layoutPosition) + } + } + } + + interface Callback { + fun openSearchResult(searchResult: SearchResult, index: Int) + fun durChapterIndex(): Int + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt new file mode 100755 index 000000000..f5d774795 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -0,0 +1,108 @@ +package io.legado.app.ui.book.searchContent + + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.ChineseUtils +import kotlinx.coroutines.ensureActive +import kotlin.coroutines.coroutineContext + +class SearchContentViewModel(application: Application) : BaseViewModel(application) { + var bookUrl: String = "" + var book: Book? = null + private var contentProcessor: ContentProcessor? = null + var lastQuery: String = "" + var searchResultCounts = 0 + val cacheChapterNames = hashSetOf() + val searchResultList: MutableList = mutableListOf() + var replaceEnabled = false + + fun initBook(bookUrl: String, success: () -> Unit) { + this.bookUrl = bookUrl + execute { + book = appDb.bookDao.getBook(bookUrl) + book?.let { + contentProcessor = ContentProcessor.get(it.name, it.origin) + } + }.onSuccess { + success.invoke() + } + } + + suspend fun searchChapter( + query: String, + chapter: BookChapter + ): List { + val searchResultsWithinChapter: MutableList = mutableListOf() + val book = book ?: return searchResultsWithinChapter + val chapterContent = BookHelp.getContent(book, chapter) ?: return searchResultsWithinChapter + coroutineContext.ensureActive() + chapter.title = when (AppConfig.chineseConverterType) { + 1 -> ChineseUtils.t2s(chapter.title) + 2 -> ChineseUtils.s2t(chapter.title) + else -> chapter.title + } + coroutineContext.ensureActive() + val mContent = contentProcessor!!.getContent( + book, chapter, chapterContent, useReplace = replaceEnabled + ).toString() + val positions = searchPosition(mContent, query) + positions.forEachIndexed { index, position -> + coroutineContext.ensureActive() + val construct = getResultAndQueryIndex(mContent, position, query) + val result = SearchResult( + resultCountWithinChapter = index, + resultText = construct.second, + chapterTitle = chapter.title, + query = query, + chapterIndex = chapter.index, + queryIndexInResult = construct.first, + queryIndexInChapter = position + ) + searchResultsWithinChapter.add(result) + } + searchResultCounts += searchResultsWithinChapter.size + return searchResultsWithinChapter + } + + private suspend fun searchPosition(content: String, pattern: String): List { + val position: MutableList = mutableListOf() + var index = content.indexOf(pattern) + while (index >= 0) { + coroutineContext.ensureActive() + position.add(index) + index = content.indexOf(pattern, index + pattern.length) + } + return position + } + + private fun getResultAndQueryIndex( + content: String, + queryIndexInContent: Int, + query: String + ): Pair { + // 左右移动20个字符,构建关键词周边文字,在搜索结果里显示 + // 判断段落,只在关键词所在段落内分割 + // 利用标点符号分割完整的句 + // length和设置结合,自由调整周边文字长度 + val length = 20 + var po1 = queryIndexInContent - length + var po2 = queryIndexInContent + query.length + length + if (po1 < 0) { + po1 = 0 + } + if (po2 > content.length) { + po2 = content.length + } + val queryIndexInResult = queryIndexInContent - po1 + val newText = content.substring(po1, po2) + return queryIndexInResult to newText + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt new file mode 100755 index 000000000..ea24702a9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt @@ -0,0 +1,61 @@ +package io.legado.app.ui.book.searchContent + +import android.text.Spanned +import androidx.core.text.HtmlCompat +import io.legado.app.help.config.AppConfig + +data class SearchResult( + val resultCount: Int = 0, + val resultCountWithinChapter: Int = 0, + val resultText: String = "", + val chapterTitle: String = "", + val query: String = "", + val pageSize: Int = 0, + val chapterIndex: Int = 0, + val pageIndex: Int = 0, + val queryIndexInResult: Int = 0, + val queryIndexInChapter: Int = 0 +) { + + fun getHtmlCompat(textColor: String, accentColor: String): Spanned { + return if (query.isNotBlank()) { + val queryIndexInSurrounding = resultText.indexOf(query) + val leftString = resultText.substring(0, queryIndexInSurrounding) + val rightString = + resultText.substring(queryIndexInSurrounding + query.length, resultText.length) + + // 检查是否为墨水屏模式 + val html = if (AppConfig.isEInkMode) { + // 墨水屏模式:使用下划线 + buildString { + append("${chapterTitle}") + append("
    ") + append(leftString) + append("${query}") + append(rightString) + } + } else { + // 普通模式:使用颜色 + buildString { + append(chapterTitle.colorTextForHtml(accentColor)) + append("
    ") + append(leftString.colorTextForHtml(textColor)) + append(query.colorTextForHtml(accentColor)) + append(rightString.colorTextForHtml(textColor)) + } + } + HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) + } else { + val html = if (AppConfig.isEInkMode) { + resultText + } else { + resultText.colorTextForHtml(textColor) + } + HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) + } + } + + private fun String.colorTextForHtml(textColor: String) = + "$this" + +} diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt new file mode 100755 index 000000000..215d796ab --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugActivity.kt @@ -0,0 +1,215 @@ +package io.legado.app.ui.book.source.debug + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivitySourceDebugBinding +import io.legado.app.help.source.clearExploreKindsCache +import io.legado.app.help.source.exploreKinds +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.launch +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.launch +import splitties.views.onClick +import splitties.views.onLongClick + +class BookSourceDebugActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivitySourceDebugBinding::inflate) + override val viewModel by viewModels() + + private val adapter by lazy { BookSourceDebugAdapter(this) } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it?.let { + startSearch(it) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initRecyclerView() + initSearchView() + viewModel.init(intent.getStringExtra("key")) { + initHelpView() + } + viewModel.observe { state, msg -> + lifecycleScope.launch { + adapter.addItem(msg) + if (state == -1 || state == 1000) { + binding.rotateLoading.gone() + } + } + } + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + binding.rotateLoading.loadingColor = accentColor + } + + private fun initSearchView() { + searchView.onActionViewExpanded() + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.search_book_key) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + searchView.clearFocus() + openOrCloseHelp(false) + startSearch(query ?: "我的") + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + }) + searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> + openOrCloseHelp(hasFocus) + } + openOrCloseHelp(true) + } + + @SuppressLint("SetTextI18n") + private fun initHelpView() { + viewModel.bookSource?.ruleSearch?.checkKeyWord?.let { + if (it.isNotBlank()) { + binding.textMy.text = it + } + } + binding.textMy.onClick { + searchView.setQuery(binding.textMy.text, true) + } + binding.textXt.onClick { + searchView.setQuery(binding.textXt.text, true) + } + binding.textFx.onClick { + if (!binding.textFx.text.startsWith("ERROR:")) { + searchView.setQuery(binding.textFx.text, true) + } + } + binding.textInfo.onClick { + if (!searchView.query.isNullOrBlank()) { + searchView.setQuery(searchView.query, true) + } + } + binding.textToc.onClick { + prefixAutoComplete("++") + } + binding.textContent.onClick { + prefixAutoComplete("--") + } + initExploreKinds() + } + + @SuppressLint("SetTextI18n") + private fun initExploreKinds() { + lifecycleScope.launch { + try { + val exploreKinds = viewModel.bookSource?.exploreKinds()?.filter { + !it.url.isNullOrBlank() + } + exploreKinds?.firstOrNull()?.let { + binding.textFx.text = "${it.title}::${it.url}" + if (it.title.startsWith("ERROR:")) { + adapter.addItem("获取发现出错\n${it.url}") + openOrCloseHelp(false) + searchView.clearFocus() + return@launch + } + } + @Suppress("USELESS_ELVIS") + exploreKinds?.map { it.title ?: "" }?.let { exploreKindTitles -> + binding.textFx.onLongClick { + selector("选择发现", exploreKindTitles) { _, index -> + val explore = exploreKinds[index] + binding.textFx.text = "${explore.title}::${explore.url}" + searchView.setQuery(binding.textFx.text, true) + } + } + } + } catch (e: NullPointerException) { + adapter.addItem("获取发现出错 JSON 数据错误\n$e") + openOrCloseHelp(false) + searchView.clearFocus() + } + } + } + + private fun prefixAutoComplete(prefix: String) { + val query = searchView.query + if (query.isNullOrBlank() || query.length <= 2) { + searchView.setQuery(prefix, false) + } else { + if (!query.startsWith(prefix)) { + searchView.setQuery("$prefix$query", true) + } else { + searchView.setQuery(query, true) + } + } + } + + /** + * 打开关闭历史界面 + */ + private fun openOrCloseHelp(open: Boolean) { + if (open) { + binding.help.visibility = View.VISIBLE + } else { + binding.help.visibility = View.GONE + } + } + + private fun startSearch(key: String) { + adapter.clearItems() + viewModel.startDebug(key, { + binding.rotateLoading.visible() + }, { + toastOnUi("未获取到书源") + }) + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_source_debug, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_scan -> qrCodeResult.launch() + R.id.menu_search_src -> showDialogFragment(TextDialog("html", viewModel.searchSrc)) + R.id.menu_book_src -> showDialogFragment(TextDialog("html", viewModel.bookSrc)) + R.id.menu_toc_src -> showDialogFragment(TextDialog("html", viewModel.tocSrc)) + R.id.menu_content_src -> showDialogFragment(TextDialog("html", viewModel.contentSrc)) + R.id.menu_refresh_explore -> lifecycleScope.launch { + viewModel.bookSource?.clearExploreKindsCache() + adapter.clearItems() + openOrCloseHelp(true) + initExploreKinds() + } + + R.id.menu_help -> showHelp("debugHelp") + } + return super.onCompatOptionsItemSelected(item) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt new file mode 100755 index 000000000..dd93b0a59 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt @@ -0,0 +1,44 @@ +package io.legado.app.ui.book.source.debug + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemLogBinding + +class BookSourceDebugAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemLogBinding { + return ItemLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemLogBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + if (textView.getTag(R.id.tag1) == null) { + val listener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + textView.isCursorVisible = false + textView.isCursorVisible = true + } + + override fun onViewDetachedFromWindow(v: View) {} + } + textView.addOnAttachStateChangeListener(listener) + textView.setTag(R.id.tag1, listener) + } + textView.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) { + //nothing + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt new file mode 100755 index 000000000..b7efa3029 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt @@ -0,0 +1,60 @@ +package io.legado.app.ui.book.source.debug + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.model.Debug + +class BookSourceDebugModel(application: Application) : BaseViewModel(application), + Debug.Callback { + + var bookSource: BookSource? = null + private var callback: ((Int, String) -> Unit)? = null + var searchSrc: String? = null + var bookSrc: String? = null + var tocSrc: String? = null + var contentSrc: String? = null + + fun init(sourceUrl: String?, finally: () -> Unit) { + sourceUrl?.let { + //优先使用这个,不会抛出异常 + execute { + bookSource = appDb.bookSourceDao.getBookSource(sourceUrl) + }.onFinally { + finally.invoke() + } + } + } + + fun observe(callback: (Int, String) -> Unit) { + this.callback = callback + } + + fun startDebug(key: String, start: (() -> Unit)? = null, error: (() -> Unit)? = null) { + execute { + Debug.callback = this@BookSourceDebugModel + Debug.startDebug(this, bookSource!!, key) + }.onStart { + start?.invoke() + }.onError { + error?.invoke() + } + } + + override fun printLog(state: Int, msg: String) { + when (state) { + 10 -> searchSrc = msg + 20 -> bookSrc = msg + 30 -> tocSrc = msg + 40 -> contentSrc = msg + else -> callback?.invoke(state, msg) + } + } + + override fun onCleared() { + super.onCleared() + Debug.cancelDebug(true) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt new file mode 100755 index 000000000..d36eec510 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -0,0 +1,665 @@ +package io.legado.app.ui.book.source.edit + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.EditText +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.tabs.TabLayout +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.BookSourceType +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.rule.BookInfoRule +import io.legado.app.data.entities.rule.ContentRule +import io.legado.app.data.entities.rule.ExploreRule +import io.legado.app.data.entities.rule.SearchRule +import io.legado.app.data.entities.rule.TocRule +import io.legado.app.databinding.ActivityBookSourceEditBinding +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.book.search.SearchScope +import io.legado.app.ui.book.source.debug.BookSourceDebugActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.dialog.UrlOptionDialog +import io.legado.app.ui.widget.dialog.VariableDialog +import io.legado.app.ui.widget.keyboard.KeyboardToolPop +import io.legado.app.ui.widget.recycler.NoChildScrollLinearLayoutManager +import io.legado.app.ui.widget.text.EditEntity +import io.legado.app.utils.GSON +import io.legado.app.utils.imeHeight +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.launch +import io.legado.app.utils.navigationBarHeight +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.share +import io.legado.app.utils.shareWithQr +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.views.bottomPadding + +class BookSourceEditActivity : + VMBaseActivity(), + KeyboardToolPop.CallBack, + VariableDialog.Callback { + + override val binding by viewBinding(ActivityBookSourceEditBinding::inflate) + override val viewModel by viewModels() + + private val adapter by lazy { BookSourceEditAdapter() } + private val sourceEntities: ArrayList = ArrayList() + private val searchEntities: ArrayList = ArrayList() + private val exploreEntities: ArrayList = ArrayList() + private val infoEntities: ArrayList = ArrayList() + private val tocEntities: ArrayList = ArrayList() + private val contentEntities: ArrayList = ArrayList() + + // private val reviewEntities: ArrayList = ArrayList() + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + viewModel.importSource(it) { source -> + upSourceView(source) + } + } + private val selectDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + if (uri.isContentScheme()) { + sendText(uri.toString()) + } else { + sendText(uri.path.toString()) + } + } + } + + private val softKeyboardTool by lazy { + KeyboardToolPop(this, lifecycleScope, binding.root, this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + softKeyboardTool.attachToWindow(window) + initView() + viewModel.initData(intent) { + upSourceView(viewModel.bookSource) + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + if (!LocalConfig.ruleHelpVersionIsLast) { + showHelp("ruleHelp") + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.source_edit, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_login)?.isVisible = !getSource().loginUrl.isNullOrBlank() + menu.findItem(R.id.menu_auto_complete)?.isChecked = viewModel.autoComplete + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> viewModel.save(getSource()) { + setResult(RESULT_OK, Intent().putExtra("origin", it.bookSourceUrl)) + finish() + } + + R.id.menu_debug_source -> viewModel.save(getSource()) { source -> + startActivity { + putExtra("key", source.bookSourceUrl) + } + } + + R.id.menu_clear_cookie -> viewModel.clearCookie(getSource().bookSourceUrl) + R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete + R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource())) + R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) } + R.id.menu_qr_code_camera -> qrCodeResult.launch() + R.id.menu_share_str -> share(GSON.toJson(getSource())) + R.id.menu_share_qr -> shareWithQr( + GSON.toJson(getSource()), + getString(R.string.share_book_source), + ErrorCorrectionLevel.L + ) + + R.id.menu_help -> showHelp("ruleHelp") + R.id.menu_login -> viewModel.save(getSource()) { source -> + startActivity { + putExtra("type", "bookSource") + putExtra("key", source.bookSourceUrl) + } + } + + R.id.menu_set_source_variable -> setSourceVariable() + R.id.menu_search -> viewModel.save(getSource()) { source -> + startActivity { + putExtra("searchScope", SearchScope(source).toString()) + } + } + + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_base) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_search) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_find) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_info) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_toc) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_content) + }) + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.layoutManager = NoChildScrollLinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.tabLayout.setBackgroundColor(backgroundColor) + binding.tabLayout.setSelectedTabIndicatorColor(accentColor) + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab?) { + + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + + } + + override fun onTabSelected(tab: TabLayout.Tab?) { + setEditEntities(tab?.position) + } + }) + binding.recyclerView.setOnApplyWindowInsetsListenerCompat { view, windowInsets -> + val navigationBarHeight = windowInsets.navigationBarHeight + val imeHeight = windowInsets.imeHeight + view.bottomPadding = if (imeHeight == 0) navigationBarHeight else 0 + softKeyboardTool.initialPadding = imeHeight + windowInsets + } + } + + override fun finish() { + val source = getSource() + if (!source.equal(viewModel.bookSource ?: BookSource())) { + alert(R.string.exit) { + setMessage(R.string.exit_no_save) + positiveButton(R.string.yes) + negativeButton(R.string.no) { + super.finish() + } + } + } else { + super.finish() + } + } + + override fun onDestroy() { + super.onDestroy() + softKeyboardTool.dismiss() + } + + private fun setEditEntities(tabPosition: Int?) { + adapter.editEntities = when (tabPosition) { + 1 -> searchEntities + 2 -> exploreEntities + 3 -> infoEntities + 4 -> tocEntities + 5 -> contentEntities +// 6 -> reviewEntities + else -> sourceEntities + } + binding.recyclerView.scrollToPosition(0) + } + + private fun upSourceView(bookSource: BookSource?) { + val bs = bookSource ?: BookSource() + bs.let { + binding.cbIsEnable.isChecked = it.enabled + binding.cbIsEnableExplore.isChecked = it.enabledExplore + binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false + binding.spType.setSelection( + when (it.bookSourceType) { + BookSourceType.file -> 3 + BookSourceType.image -> 2 + BookSourceType.audio -> 1 + else -> 0 + } + ) + } + // 基本信息 + sourceEntities.clear() + sourceEntities.apply { + add(EditEntity("bookSourceUrl", bs.bookSourceUrl, R.string.source_url)) + add(EditEntity("bookSourceName", bs.bookSourceName, R.string.source_name)) + add(EditEntity("bookSourceGroup", bs.bookSourceGroup, R.string.source_group)) + add(EditEntity("bookSourceComment", bs.bookSourceComment, R.string.comment)) + add(EditEntity("loginUrl", bs.loginUrl, R.string.login_url)) + add(EditEntity("loginUi", bs.loginUi, R.string.login_ui)) + add(EditEntity("loginCheckJs", bs.loginCheckJs, R.string.login_check_js)) + add(EditEntity("coverDecodeJs", bs.coverDecodeJs, R.string.cover_decode_js)) + add(EditEntity("bookUrlPattern", bs.bookUrlPattern, R.string.book_url_pattern)) + add(EditEntity("header", bs.header, R.string.source_http_header)) + add(EditEntity("variableComment", bs.variableComment, R.string.variable_comment)) + add(EditEntity("concurrentRate", bs.concurrentRate, R.string.concurrent_rate)) + add(EditEntity("jsLib", bs.jsLib, "jsLib")) + } + // 搜索 + val sr = bs.getSearchRule() + searchEntities.clear() + searchEntities.apply { + add(EditEntity("searchUrl", bs.searchUrl, R.string.r_search_url)) + add(EditEntity("checkKeyWord", sr.checkKeyWord, R.string.check_key_word)) + add(EditEntity("bookList", sr.bookList, R.string.r_book_list)) + add(EditEntity("name", sr.name, R.string.r_book_name)) + add(EditEntity("author", sr.author, R.string.r_author)) + add(EditEntity("kind", sr.kind, R.string.rule_book_kind)) + add(EditEntity("wordCount", sr.wordCount, R.string.rule_word_count)) + add(EditEntity("lastChapter", sr.lastChapter, R.string.rule_last_chapter)) + add(EditEntity("intro", sr.intro, R.string.rule_book_intro)) + add(EditEntity("coverUrl", sr.coverUrl, R.string.rule_cover_url)) + add(EditEntity("bookUrl", sr.bookUrl, R.string.r_book_url)) + } + // 发现 + val er = bs.getExploreRule() + exploreEntities.clear() + exploreEntities.apply { + add(EditEntity("exploreUrl", bs.exploreUrl, R.string.r_find_url)) + add(EditEntity("bookList", er.bookList, R.string.r_book_list)) + add(EditEntity("name", er.name, R.string.r_book_name)) + add(EditEntity("author", er.author, R.string.r_author)) + add(EditEntity("kind", er.kind, R.string.rule_book_kind)) + add(EditEntity("wordCount", er.wordCount, R.string.rule_word_count)) + add(EditEntity("lastChapter", er.lastChapter, R.string.rule_last_chapter)) + add(EditEntity("intro", er.intro, R.string.rule_book_intro)) + add(EditEntity("coverUrl", er.coverUrl, R.string.rule_cover_url)) + add(EditEntity("bookUrl", er.bookUrl, R.string.r_book_url)) + } + // 详情页 + val ir = bs.getBookInfoRule() + infoEntities.clear() + infoEntities.apply { + add(EditEntity("init", ir.init, R.string.rule_book_info_init)) + add(EditEntity("name", ir.name, R.string.r_book_name)) + add(EditEntity("author", ir.author, R.string.r_author)) + add(EditEntity("kind", ir.kind, R.string.rule_book_kind)) + add(EditEntity("wordCount", ir.wordCount, R.string.rule_word_count)) + add(EditEntity("lastChapter", ir.lastChapter, R.string.rule_last_chapter)) + add(EditEntity("intro", ir.intro, R.string.rule_book_intro)) + add(EditEntity("coverUrl", ir.coverUrl, R.string.rule_cover_url)) + add(EditEntity("tocUrl", ir.tocUrl, R.string.rule_toc_url)) + add(EditEntity("canReName", ir.canReName, R.string.rule_can_re_name)) + add(EditEntity("downloadUrls", ir.downloadUrls, R.string.download_url_rule)) + } + // 目录页 + val tr = bs.getTocRule() + tocEntities.clear() + tocEntities.apply { + add(EditEntity("preUpdateJs", tr.preUpdateJs, R.string.pre_update_js)) + add(EditEntity("chapterList", tr.chapterList, R.string.rule_chapter_list)) + add(EditEntity("chapterName", tr.chapterName, R.string.rule_chapter_name)) + add(EditEntity("chapterUrl", tr.chapterUrl, R.string.rule_chapter_url)) + add(EditEntity("formatJs", tr.formatJs, R.string.format_js_rule)) + add(EditEntity("isVolume", tr.isVolume, R.string.rule_is_volume)) + add(EditEntity("updateTime", tr.updateTime, R.string.rule_update_time)) + add(EditEntity("isVip", tr.isVip, R.string.rule_is_vip)) + add(EditEntity("isPay", tr.isPay, R.string.rule_is_pay)) + add(EditEntity("nextTocUrl", tr.nextTocUrl, R.string.rule_next_toc_url)) + } + // 正文页 + val cr = bs.getContentRule() + contentEntities.clear() + contentEntities.apply { + add(EditEntity("content", cr.content, R.string.rule_book_content)) + add(EditEntity("title", cr.title, R.string.rule_chapter_name)) + add(EditEntity("nextContentUrl", cr.nextContentUrl, R.string.rule_next_content)) + add(EditEntity("webJs", cr.webJs, R.string.rule_web_js)) + add(EditEntity("sourceRegex", cr.sourceRegex, R.string.rule_source_regex)) + add(EditEntity("replaceRegex", cr.replaceRegex, R.string.rule_replace_regex)) + add(EditEntity("imageStyle", cr.imageStyle, R.string.rule_image_style)) + add(EditEntity("imageDecode", cr.imageDecode, R.string.rule_image_decode)) + add(EditEntity("payAction", cr.payAction, R.string.rule_pay_action)) + } + // 段评 +// val rr = bs.getReviewRule() +// reviewEntities.clear() +// reviewEntities.apply { +// add(EditEntity("reviewUrl", rr.reviewUrl, R.string.rule_review_url)) +// add(EditEntity("avatarRule", rr.avatarRule, R.string.rule_avatar)) +// add(EditEntity("contentRule", rr.contentRule, R.string.rule_review_content)) +// add(EditEntity("postTimeRule", rr.postTimeRule, R.string.rule_post_time)) +// add(EditEntity("reviewQuoteUrl", rr.reviewQuoteUrl, R.string.rule_review_quote)) +// add(EditEntity("voteUpUrl", rr.voteUpUrl, R.string.review_vote_up)) +// add(EditEntity("voteDownUrl", rr.voteDownUrl, R.string.review_vote_down)) +// add(EditEntity("postReviewUrl", rr.postReviewUrl, R.string.post_review_url)) +// add(EditEntity("postQuoteUrl", rr.postQuoteUrl, R.string.post_quote_url)) +// add(EditEntity("deleteUrl", rr.deleteUrl, R.string.delete_review_url)) +// } + binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0)) + setEditEntities(0) + } + + private fun getSource(): BookSource { + val source = viewModel.bookSource?.copy() ?: BookSource() + source.enabled = binding.cbIsEnable.isChecked + source.enabledExplore = binding.cbIsEnableExplore.isChecked + source.enabledCookieJar = binding.cbIsEnableCookie.isChecked + source.bookSourceType = when (binding.spType.selectedItemPosition) { + 3 -> BookSourceType.file + 2 -> BookSourceType.image + 1 -> BookSourceType.audio + else -> BookSourceType.default + } + val searchRule = SearchRule() + val exploreRule = ExploreRule() + val bookInfoRule = BookInfoRule() + val tocRule = TocRule() + val contentRule = ContentRule() +// val reviewRule = ReviewRule() + sourceEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "bookSourceUrl" -> source.bookSourceUrl = it.value ?: "" + "bookSourceName" -> source.bookSourceName = it.value ?: "" + "bookSourceGroup" -> source.bookSourceGroup = it.value + "loginUrl" -> source.loginUrl = it.value + "loginUi" -> source.loginUi = it.value + "loginCheckJs" -> source.loginCheckJs = it.value + "coverDecodeJs" -> source.coverDecodeJs = it.value + "bookUrlPattern" -> source.bookUrlPattern = it.value + "header" -> source.header = it.value + "bookSourceComment" -> source.bookSourceComment = it.value + "concurrentRate" -> source.concurrentRate = it.value + "variableComment" -> source.variableComment = it.value + "jsLib" -> source.jsLib = it.value + } + } + searchEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "searchUrl" -> source.searchUrl = it.value + "checkKeyWord" -> searchRule.checkKeyWord = it.value + "bookList" -> searchRule.bookList = it.value + "name" -> searchRule.name = + viewModel.ruleComplete(it.value, searchRule.bookList) + + "author" -> searchRule.author = + viewModel.ruleComplete(it.value, searchRule.bookList) + + "kind" -> searchRule.kind = + viewModel.ruleComplete(it.value, searchRule.bookList) + + "intro" -> searchRule.intro = + viewModel.ruleComplete(it.value, searchRule.bookList) + +// "updateTime" -> searchRule.updateTime = +// viewModel.ruleComplete(it.value, searchRule.bookList) + + "wordCount" -> searchRule.wordCount = + viewModel.ruleComplete(it.value, searchRule.bookList) + + "lastChapter" -> searchRule.lastChapter = + viewModel.ruleComplete(it.value, searchRule.bookList) + + "coverUrl" -> searchRule.coverUrl = + viewModel.ruleComplete(it.value, searchRule.bookList, 3) + + "bookUrl" -> searchRule.bookUrl = + viewModel.ruleComplete(it.value, searchRule.bookList, 2) + } + } + exploreEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "exploreUrl" -> source.exploreUrl = it.value + "bookList" -> exploreRule.bookList = it.value + "name" -> exploreRule.name = + viewModel.ruleComplete(it.value, exploreRule.bookList) + + "author" -> exploreRule.author = + viewModel.ruleComplete(it.value, exploreRule.bookList) + + "kind" -> exploreRule.kind = + viewModel.ruleComplete(it.value, exploreRule.bookList) + + "intro" -> exploreRule.intro = + viewModel.ruleComplete(it.value, exploreRule.bookList) + +// "updateTime" -> exploreRule.updateTime = +// viewModel.ruleComplete(it.value, exploreRule.bookList) + + "wordCount" -> exploreRule.wordCount = + viewModel.ruleComplete(it.value, exploreRule.bookList) + + "lastChapter" -> exploreRule.lastChapter = + viewModel.ruleComplete(it.value, exploreRule.bookList) + + "coverUrl" -> exploreRule.coverUrl = + viewModel.ruleComplete(it.value, exploreRule.bookList, 3) + + "bookUrl" -> exploreRule.bookUrl = + viewModel.ruleComplete(it.value, exploreRule.bookList, 2) + } + } + infoEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "init" -> bookInfoRule.init = it.value + "name" -> bookInfoRule.name = viewModel.ruleComplete(it.value, bookInfoRule.init) + "author" -> bookInfoRule.author = + viewModel.ruleComplete(it.value, bookInfoRule.init) + + "kind" -> bookInfoRule.kind = + viewModel.ruleComplete(it.value, bookInfoRule.init) + + "intro" -> bookInfoRule.intro = + viewModel.ruleComplete(it.value, bookInfoRule.init) + +// "updateTime" -> bookInfoRule.updateTime = +// viewModel.ruleComplete(it.value, bookInfoRule.init) + + "wordCount" -> bookInfoRule.wordCount = + viewModel.ruleComplete(it.value, bookInfoRule.init) + + "lastChapter" -> bookInfoRule.lastChapter = + viewModel.ruleComplete(it.value, bookInfoRule.init) + + "coverUrl" -> bookInfoRule.coverUrl = + viewModel.ruleComplete(it.value, bookInfoRule.init, 3) + + "tocUrl" -> bookInfoRule.tocUrl = + viewModel.ruleComplete(it.value, bookInfoRule.init, 2) + + "canReName" -> bookInfoRule.canReName = it.value + "downloadUrls" -> bookInfoRule.downloadUrls = + viewModel.ruleComplete(it.value, bookInfoRule.init) + } + } + tocEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "preUpdateJs" -> tocRule.preUpdateJs = it.value + "chapterList" -> tocRule.chapterList = it.value + "chapterName" -> tocRule.chapterName = + viewModel.ruleComplete(it.value, tocRule.chapterList) + + "chapterUrl" -> tocRule.chapterUrl = + viewModel.ruleComplete(it.value, tocRule.chapterList, 2) + + "formatJs" -> tocRule.formatJs = it.value + "isVolume" -> tocRule.isVolume = it.value + "updateTime" -> tocRule.updateTime = it.value + "isVip" -> tocRule.isVip = it.value + "isPay" -> tocRule.isPay = it.value + "nextTocUrl" -> tocRule.nextTocUrl = + viewModel.ruleComplete(it.value, tocRule.chapterList, 2) + } + } + contentEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "content" -> contentRule.content = viewModel.ruleComplete(it.value) + "title" -> contentRule.title = viewModel.ruleComplete(it.value) + "nextContentUrl" -> contentRule.nextContentUrl = + viewModel.ruleComplete(it.value, type = 2) + + "webJs" -> contentRule.webJs = it.value + "sourceRegex" -> contentRule.sourceRegex = it.value + "replaceRegex" -> contentRule.replaceRegex = it.value + "imageStyle" -> contentRule.imageStyle = it.value + "imageDecode" -> contentRule.imageDecode = it.value + "payAction" -> contentRule.payAction = it.value + } + } +// reviewEntities.forEach { +// when (it.key) { +// "reviewUrl" -> reviewRule.reviewUrl = it.value +// "avatarRule" -> reviewRule.avatarRule = +// viewModel.ruleComplete(it.value, reviewRule.reviewUrl, 3) +// +// "contentRule" -> reviewRule.contentRule = +// viewModel.ruleComplete(it.value, reviewRule.reviewUrl) +// +// "postTimeRule" -> reviewRule.postTimeRule = +// viewModel.ruleComplete(it.value, reviewRule.reviewUrl) +// +// "reviewQuoteUrl" -> reviewRule.reviewQuoteUrl = +// viewModel.ruleComplete(it.value, reviewRule.reviewUrl, 2) +// +// "voteUpUrl" -> reviewRule.voteUpUrl = it.value +// "voteDownUrl" -> reviewRule.voteDownUrl = it.value +// "postReviewUrl" -> reviewRule.postReviewUrl = it.value +// "postQuoteUrl" -> reviewRule.postQuoteUrl = it.value +// "deleteUrl" -> reviewRule.deleteUrl = it.value +// } +// } + source.ruleSearch = searchRule + source.ruleExplore = exploreRule + source.ruleBookInfo = bookInfoRule + source.ruleToc = tocRule + source.ruleContent = contentRule +// source.ruleReview = reviewRule + return source + } + + private fun alertGroups() { + lifecycleScope.launch { + val groups = withContext(IO) { + appDb.bookSourceDao.allGroups() + } + selector(groups) { _, s, _ -> + sendText(s) + } + } + } + + override fun helpActions(): List> { + val helpActions = arrayListOf( + SelectItem("插入URL参数", "urlOption"), + SelectItem("书源教程", "ruleHelp"), + SelectItem("js教程", "jsHelp"), + SelectItem("正则教程", "regexHelp"), + ) + val view = window.decorView.findFocus() + if (view is EditText) { + when (view.getTag(R.id.tag)) { + "bookSourceGroup" -> { + helpActions.add( + SelectItem("插入分组", "addGroup") + ) + } + + else -> { + helpActions.add( + SelectItem("选择文件", "selectFile") + ) + } + } + } + return helpActions + } + + override fun onHelpActionSelect(action: String) { + when (action) { + "addGroup" -> alertGroups() + "urlOption" -> UrlOptionDialog(this) { sendText(it) }.show() + "ruleHelp" -> showHelp("ruleHelp") + "jsHelp" -> showHelp("jsHelp") + "regexHelp" -> showHelp("regexHelp") + "selectFile" -> selectDoc.launch { + mode = HandleFileContract.FILE + } + } + } + + override fun sendText(text: String) { + if (text.isBlank()) return + val view = window.decorView.findFocus() + if (view is EditText) { + val start = view.selectionStart + val end = view.selectionEnd + val edit = view.editableText//获取EditText的文字 + if (start < 0 || start >= edit.length) { + edit.append(text) + } else if (start > end) { + edit.replace(end, start, text) + } else { + edit.replace(start, end, text)//光标所在位置插入文字 + } + } + } + + private fun setSourceVariable() { + viewModel.save(getSource()) { source -> + lifecycleScope.launch { + val comment = + source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取") + val variable = withContext(IO) { source.getVariable() } + showDialogFragment( + VariableDialog( + getString(R.string.set_source_variable), + source.getKey(), + variable, + comment + ) + ) + } + } + } + + override fun setVariable(key: String, variable: String?) { + viewModel.bookSource?.setVariable(variable) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt new file mode 100755 index 000000000..b5b65be0a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt @@ -0,0 +1,99 @@ +package io.legado.app.ui.book.source.edit + +import android.annotation.SuppressLint +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.databinding.ItemSourceEditBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.ui.widget.code.addJsPattern +import io.legado.app.ui.widget.code.addJsonPattern +import io.legado.app.ui.widget.code.addLegadoPattern +import io.legado.app.ui.widget.text.EditEntity + +class BookSourceEditAdapter : RecyclerView.Adapter() { + + val editEntityMaxLine = AppConfig.sourceEditMaxLine + + var editEntities: ArrayList = ArrayList() + @SuppressLint("NotifyDataSetChanged") + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val binding = ItemSourceEditBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + binding.editText.addLegadoPattern() + binding.editText.addJsonPattern() + binding.editText.addJsPattern() + return MyViewHolder(binding) + } + + override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + holder.bind(editEntities[position]) + } + + override fun getItemCount(): Int { + return editEntities.size + } + + inner class MyViewHolder(val binding: ItemSourceEditBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(editEntity: EditEntity) = binding.run { + editText.setTag(R.id.tag, editEntity.key) + editText.maxLines = editEntityMaxLine + if (editText.getTag(R.id.tag1) == null) { + val listener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + editText.isCursorVisible = false + editText.isCursorVisible = true + editText.isFocusable = true + editText.isFocusableInTouchMode = true + } + + override fun onViewDetachedFromWindow(v: View) { + + } + } + editText.addOnAttachStateChangeListener(listener) + editText.setTag(R.id.tag1, listener) + } + editText.getTag(R.id.tag2)?.let { + if (it is TextWatcher) { + editText.removeTextChangedListener(it) + } + } + editText.setText(editEntity.value) + textInputLayout.hint = editEntity.hint + val textWatcher = object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + + } + + override fun afterTextChanged(s: Editable?) { + editEntity.value = (s?.toString()) + } + } + editText.addTextChangedListener(textWatcher) + editText.setTag(R.id.tag2, textWatcher) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt new file mode 100755 index 000000000..a629f497d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt @@ -0,0 +1,153 @@ +package io.legado.app.ui.book.source.edit + +import android.app.Application +import android.content.Intent +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.RuleComplete +import io.legado.app.help.config.SourceConfig +import io.legado.app.help.http.CookieStore +import io.legado.app.help.http.newCallStrResponse +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.source.SourceHelp +import io.legado.app.help.source.clearExploreKindsCache +import io.legado.app.help.storage.ImportOldData +import io.legado.app.model.SharedJsScope +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getClipText +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.jsonPath +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers + + +class BookSourceEditViewModel(application: Application) : BaseViewModel(application) { + var autoComplete = false + var bookSource: BookSource? = null + + fun initData(intent: Intent, onFinally: () -> Unit) { + execute { + val sourceUrl = intent.getStringExtra("sourceUrl") + var source: BookSource? = null + if (sourceUrl != null) { + source = appDb.bookSourceDao.getBookSource(sourceUrl) + } + source?.let { + bookSource = it + } + }.onFinally { + onFinally() + } + } + + fun save(source: BookSource, success: ((BookSource) -> Unit)? = null) { + execute { + if (source.bookSourceUrl.isBlank() || source.bookSourceName.isBlank()) { + throw NoStackTraceException(context.getString(R.string.non_null_name_url)) + } + val oldSource = bookSource ?: BookSource() + if (!source.equal(oldSource)) { + source.lastUpdateTime = System.currentTimeMillis() + if (oldSource.exploreUrl != source.exploreUrl) { + oldSource.clearExploreKindsCache() + } + if (oldSource.jsLib != source.jsLib) { + SharedJsScope.remove(oldSource.jsLib) + } + } + bookSource?.let { + if (it.bookSourceUrl != source.bookSourceUrl) { + SourceHelp.deleteBookSource(it.bookSourceUrl) + } else { + appDb.bookSourceDao.delete(it) + SourceConfig.removeSource(it.bookSourceUrl) + } + } + appDb.bookSourceDao.insert(source) + bookSource = source + source + }.onSuccess { + success?.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage) + it.printOnDebug() + } + } + + fun pasteSource(onSuccess: (source: BookSource) -> Unit) { + execute(context = Dispatchers.Main) { + val text = context.getClipText() + if (text.isNullOrBlank()) { + throw NoStackTraceException("剪贴板为空") + } else { + importSource(text, onSuccess) + } + }.onError { + context.toastOnUi(it.localizedMessage ?: "Error") + it.printOnDebug() + } + } + + fun importSource(text: String, finally: (source: BookSource) -> Unit) { + execute { + importSource(text) + }.onSuccess { + finally.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage ?: "Error") + it.printOnDebug() + } + } + + suspend fun importSource(text: String): BookSource { + return when { + text.isAbsUrl() -> { + val text1 = okHttpClient.newCallStrResponse { url(text) }.body + importSource(text1!!) + } + + text.isJsonArray() -> { + if (text.contains("ruleSearchUrl") || text.contains("ruleFindUrl")) { + val items: List> = jsonPath.parse(text).read("$") + val jsonItem = jsonPath.parse(items[0]) + ImportOldData.fromOldBookSource(jsonItem) + } else { + GSON.fromJsonArray(text).getOrThrow()[0] + } + } + + text.isJsonObject() -> { + if (text.contains("ruleSearchUrl") || text.contains("ruleFindUrl")) { + val jsonItem = jsonPath.parse(text) + ImportOldData.fromOldBookSource(jsonItem) + } else { + GSON.fromJsonObject(text).getOrThrow() + } + } + + else -> throw NoStackTraceException("格式不对") + } + } + + fun clearCookie(url: String) { + execute { + CookieStore.removeCookie(url) + } + } + + fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? { + if (autoComplete) { + return RuleComplete.autoComplete(rule, preRule, type) + } + return rule + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt new file mode 100755 index 000000000..b3bdd817b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -0,0 +1,780 @@ +package io.legado.app.ui.book.source.manage + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import android.view.WindowManager +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.ItemTouchHelper +import com.google.android.material.snackbar.Snackbar +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.databinding.ActivityBookSourceBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.model.CheckSource +import io.legado.app.model.Debug +import io.legado.app.ui.association.ImportBookSourceDialog +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.book.search.SearchScope +import io.legado.app.ui.book.source.debug.BookSourceDebugActivity +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.config.CheckSourceConfig +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ACache +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.applyTint +import io.legado.app.utils.cnCompare +import io.legado.app.utils.dpToPx +import io.legado.app.utils.flowWithLifecycleAndDatabaseChange +import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.launch +import io.legado.app.utils.observeEvent +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.share +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +/** + * 书源管理界面 + */ +class BookSourceActivity : VMBaseActivity(), + PopupMenu.OnMenuItemClickListener, + BookSourceAdapter.CallBack, + SelectActionBar.CallBack, + SearchView.OnQueryTextListener { + override val binding by viewBinding(ActivityBookSourceBinding::inflate) + override val viewModel by viewModels() + private val importRecordKey = "bookSourceRecordKey" + private val adapter by lazy { BookSourceAdapter(this, this, binding.recyclerView) } + private val itemTouchCallback by lazy { ItemTouchCallback(adapter) } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var sourceFlowJob: Job? = null + private var checkMessageRefreshJob: Job? = null + private val groups = linkedSetOf() + private var groupMenu: SubMenu? = null + override var sort = BookSourceSort.Default + private set + override var sortAscending = true + private set + private var snackBar: Snackbar? = null + private var groupSourcesByDomain = false + private val hostMap = hashMapOf() + private val qrResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + showDialogFragment(ImportBookSourceDialog(it)) + } + private val importDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportBookSourceDialog(uri.toString())) + } + } + private val exportDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + sendToClip(uri.toString()) + } + } + } + } + private val groupMenuLifecycleOwner = object : LifecycleOwner { + private val registry = LifecycleRegistry(this) + override val lifecycle: Lifecycle get() = registry + + fun onMenuOpened() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_START) + } + + fun onMenuClosed() { + registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } + + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initRecyclerView() + initSearchView() + upBookSource() + initLiveDataGroup() + initSelectActionBar() + resumeCheckSource() + if (!LocalConfig.bookSourcesHelpVersionIsLast) { + showHelp("SourceMBookHelp") + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_source, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + groupMenu = menu.findItem(R.id.menu_group).subMenu + val sortSubMenu = menu.findItem(R.id.action_sort).subMenu!! + sortSubMenu.findItem(R.id.menu_sort_desc).isChecked = !sortAscending + sortSubMenu.setGroupCheckable(R.id.menu_group_sort, true, true) + upGroupMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add_book_source -> startActivity() + R.id.menu_import_qr -> qrResult.launch() + R.id.menu_group_manage -> showDialogFragment() + R.id.menu_import_local -> importDoc.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_onLine -> showImportDialog() + + R.id.menu_sort_desc -> { + sortAscending = !sortAscending + item.isChecked = !sortAscending + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_manual -> { + item.isChecked = true + sort = BookSourceSort.Default + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_auto -> { + item.isChecked = true + sort = BookSourceSort.Weight + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_name -> { + item.isChecked = true + sort = BookSourceSort.Name + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_url -> { + item.isChecked = true + sort = BookSourceSort.Url + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_time -> { + item.isChecked = true + sort = BookSourceSort.Update + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_respondTime -> { + item.isChecked = true + sort = BookSourceSort.Respond + upBookSource(searchView.query?.toString()) + } + + R.id.menu_sort_enable -> { + item.isChecked = true + sort = BookSourceSort.Enable + upBookSource(searchView.query?.toString()) + } + + R.id.menu_enabled_group -> { + searchView.setQuery(getString(R.string.enabled), true) + } + + R.id.menu_disabled_group -> { + searchView.setQuery(getString(R.string.disabled), true) + } + + R.id.menu_group_login -> { + searchView.setQuery(getString(R.string.need_login), true) + } + + R.id.menu_group_null -> { + searchView.setQuery(getString(R.string.no_group), true) + } + + R.id.menu_enabled_explore_group -> { + searchView.setQuery(getString(R.string.enabled_explore), true) + } + + R.id.menu_disabled_explore_group -> { + searchView.setQuery(getString(R.string.disabled_explore), true) + } + + R.id.menu_group_sources_by_domain -> { + item.isChecked = !item.isChecked + groupSourcesByDomain = item.isChecked + adapter.showSourceHost = item.isChecked + upBookSource(searchView.query?.toString()) + } + + R.id.menu_help -> showHelp("SourceMBookHelp") + } + if (item.groupId == R.id.source_group) { + searchView.setQuery("group:${item.title}", true) + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter + binding.recyclerView.recycledViewPool.setMaxRecycledViews(0, 15) + // When this page is opened, it is in selection mode + val dragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + dragSelectTouchHelper.activeSlideSelect() + // Note: need judge selection first, so add ItemTouchHelper after it. + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.queryHint = getString(R.string.search_book_source) + searchView.setOnQueryTextListener(this) + } + + + private fun upBookSource(searchKey: String? = null) { + sourceFlowJob?.cancel() + sourceFlowJob = lifecycleScope.launch { + when { + searchKey.isNullOrEmpty() -> { + appDb.bookSourceDao.flowAll() + } + + searchKey == getString(R.string.enabled) -> { + appDb.bookSourceDao.flowEnabled() + } + + searchKey == getString(R.string.disabled) -> { + appDb.bookSourceDao.flowDisabled() + } + + searchKey == getString(R.string.need_login) -> { + appDb.bookSourceDao.flowLogin() + } + + searchKey == getString(R.string.no_group) -> { + appDb.bookSourceDao.flowNoGroup() + } + + searchKey == getString(R.string.enabled_explore) -> { + appDb.bookSourceDao.flowEnabledExplore() + } + + searchKey == getString(R.string.disabled_explore) -> { + appDb.bookSourceDao.flowDisabledExplore() + } + + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.bookSourceDao.flowGroupSearch(key) + } + + else -> { + appDb.bookSourceDao.flowSearch(searchKey) + } + }.map { data -> + hostMap.clear() + if (groupSourcesByDomain) { + data.sortedWith( + compareBy { getSourceHost(it.bookSourceUrl) == "#" } + .thenBy { getSourceHost(it.bookSourceUrl) } + .thenByDescending { it.lastUpdateTime }) + } else if (sortAscending) { + when (sort) { + BookSourceSort.Weight -> data.sortedBy { it.weight } + BookSourceSort.Name -> data.sortedWith { o1, o2 -> + o1.bookSourceName.cnCompare(o2.bookSourceName) + } + + BookSourceSort.Url -> data.sortedBy { it.bookSourceUrl } + BookSourceSort.Update -> data.sortedByDescending { it.lastUpdateTime } + BookSourceSort.Respond -> data.sortedBy { it.respondTime } + BookSourceSort.Enable -> data.sortedWith { o1, o2 -> + var sort = -o1.enabled.compareTo(o2.enabled) + if (sort == 0) { + sort = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sort + } + + else -> data + } + } else { + when (sort) { + BookSourceSort.Weight -> data.sortedByDescending { it.weight } + BookSourceSort.Name -> data.sortedWith { o1, o2 -> + o2.bookSourceName.cnCompare(o1.bookSourceName) + } + + BookSourceSort.Url -> data.sortedByDescending { it.bookSourceUrl } + BookSourceSort.Update -> data.sortedBy { it.lastUpdateTime } + BookSourceSort.Respond -> data.sortedByDescending { it.respondTime } + BookSourceSort.Enable -> data.sortedWith { o1, o2 -> + var sort = o1.enabled.compareTo(o2.enabled) + if (sort == 0) { + sort = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sort + } + + else -> data.reversed() + } + } + }.flowWithLifecycleAndDatabaseChange( + lifecycle, + table = AppDatabase.BOOK_SOURCE_TABLE_NAME + ).catch { + AppLog.put("书源界面更新书源出错", it) + }.flowOn(IO).conflate().collect { data -> + adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking) + itemTouchCallback.isCanDrag = + sort == BookSourceSort.Default && !groupSourcesByDomain + delay(500) + } + } + } + + private fun initLiveDataGroup() { + lifecycleScope.launch { + appDb.bookSourceDao.flowGroups() + .flowWithLifecycleAndDatabaseChange( + lifecycle, + table = AppDatabase.BOOK_SOURCE_TABLE_NAME + ) + .flowWithLifecycleAndDatabaseChangeFirst( + groupMenuLifecycleOwner.lifecycle, + table = AppDatabase.BOOK_SOURCE_TABLE_NAME + ) + .conflate() + .distinctUntilChanged() + .collect { + groups.clear() + groups.addAll(it) + upGroupMenu() + delay(500) + } + } + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickSelectBarMainAction() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + yesButton { viewModel.del(adapter.selection) } + noButton() + } + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + if (menu === groupMenu) { + groupMenuLifecycleOwner.onMenuOpened() + } + return super.onMenuOpened(featureId, menu) + } + + override fun onPanelClosed(featureId: Int, menu: Menu) { + super.onPanelClosed(featureId, menu) + if (menu === groupMenu) { + groupMenuLifecycleOwner.onMenuClosed() + } + } + + private fun initSelectActionBar() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.book_source_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection) + R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.selection) + R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.selection) + R.id.menu_check_source -> checkSource() + R.id.menu_top_sel -> viewModel.topSource(*adapter.selection.toTypedArray()) + R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray()) + R.id.menu_add_group -> selectionAddToGroups() + R.id.menu_remove_group -> selectionRemoveFromGroups() + R.id.menu_export_selection -> viewModel.saveToFile( + adapter, + searchView.query?.toString(), + sortAscending, + sort + ) { file -> + exportDir.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "bookSource.json", + file, + "application/json" + ) + } + } + + R.id.menu_share_source -> viewModel.saveToFile( + adapter, + searchView.query?.toString(), + sortAscending, + sort + ) { + share(it) + } + + R.id.menu_check_selected_interval -> adapter.checkSelectedInterval() + } + return true + } + + @SuppressLint("InflateParams") + private fun checkSource() { + val dialog = alert(titleResource = R.string.search_book_key) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "search word" + editView.setText(CheckSource.keyword) + } + customView { alertBinding.root } + okButton { + keepScreenOn(true) + alertBinding.editView.text?.toString()?.let { + if (it.isNotEmpty()) { + CheckSource.keyword = it + } + } + val selectItems = adapter.selection + CheckSource.start(this@BookSourceActivity, selectItems) + val adapterItems = adapter.getItems() + val firstItem = adapterItems.indexOf(selectItems.firstOrNull()) + val lastItem = adapterItems.indexOf(selectItems.lastOrNull()) + Debug.isChecking = firstItem >= 0 && lastItem >= 0 + startCheckMessageRefreshJob(firstItem, lastItem) + } + neutralButton(R.string.check_source_config) + cancelButton() + } + //手动设置监听 避免点击打开校验设置后对话框关闭 + dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { + showDialogFragment() + } + } + + private fun resumeCheckSource() { + if (!Debug.isChecking) { + return + } + keepScreenOn(true) + CheckSource.resume(this) + startCheckMessageRefreshJob(0, 0) + } + + @SuppressLint("InflateParams") + private fun selectionAddToGroups() { + alert(titleResource = R.string.add_group) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotEmpty()) { + viewModel.selectionAddToGroups(adapter.selection, it) + } + } + } + cancelButton() + } + } + + @SuppressLint("InflateParams") + private fun selectionRemoveFromGroups() { + alert(titleResource = R.string.remove_group) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotEmpty()) { + viewModel.selectionRemoveFromGroups(adapter.selection, it) + } + } + } + cancelButton() + } + } + + private fun upGroupMenu() = groupMenu?.transaction { menu -> + menu.removeGroup(R.id.source_group) + groups.forEach { + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, it) + } + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(importRecordKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (it.isAbsUrl() && !cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + showDialogFragment(ImportBookSourceDialog(it)) + } + } + cancelButton() + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.CHECK_SOURCE) { msg -> + snackBar?.setText(msg) ?: let { + snackBar = Snackbar + .make(binding.root, msg, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.cancel) { + CheckSource.stop(this) + Debug.finishChecking() + }.apply { show() } + } + } + observeEvent(EventBus.CHECK_SOURCE_DONE) { + keepScreenOn(false) + snackBar?.dismiss() + snackBar = null + adapter.notifyItemRangeChanged( + 0, + adapter.itemCount, + bundleOf(Pair("checkSourceMessage", null)) + ) + groups.forEach { group -> + if (group.contains("失效") && searchView.query.isEmpty()) { + searchView.setQuery("失效", true) + toastOnUi("发现有失效书源,已为您自动筛选!") + } + } + } + } + + private fun startCheckMessageRefreshJob(firstItem: Int, lastItem: Int) { + checkMessageRefreshJob?.cancel() + checkMessageRefreshJob = lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + while (isActive) { + if (lastItem == 0) { + adapter.notifyItemRangeChanged( + 0, + adapter.itemCount, + bundleOf(Pair("checkSourceMessage", null)) + ) + } else { + adapter.notifyItemRangeChanged( + firstItem, + lastItem + 1, + bundleOf(Pair("checkSourceMessage", null)) + ) + } + if (!Debug.isChecking) { + checkMessageRefreshJob?.cancel() + } + delay(300L) + } + } + } + } + + /** + * 保持亮屏 + */ + private fun keepScreenOn(on: Boolean) { + val isScreenOn = + (window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0 + if (on == isScreenOn) return + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + + override fun upCountView() { + binding.selectActionBar + .upCountView(adapter.selection.size, adapter.itemCount) + } + + override fun getSourceHost(origin: String): String { + return hostMap.getOrPut(origin) { + NetworkUtils.getSubDomainOrNull(origin) ?: "#" + } + } + + override fun onQueryTextChange(newText: String?): Boolean { + newText?.let { + upBookSource(it) + } + return false + } + + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun del(bookSource: BookSourcePart) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + bookSource.bookSourceName) + noButton() + yesButton { + viewModel.del(listOf(bookSource)) + } + } + } + + override fun edit(bookSource: BookSourcePart) { + startActivity { + putExtra("sourceUrl", bookSource.bookSourceUrl) + } + } + + override fun upOrder(items: List) { + viewModel.upOrder(items) + } + + override fun enable(enable: Boolean, bookSource: BookSourcePart) { + viewModel.enable(enable, listOf(bookSource)) + } + + override fun enableExplore(enable: Boolean, bookSource: BookSourcePart) { + viewModel.enableExplore(enable, listOf(bookSource)) + } + + override fun toTop(bookSource: BookSourcePart) { + if (sortAscending) { + viewModel.topSource(bookSource) + } else { + viewModel.bottomSource(bookSource) + } + } + + override fun toBottom(bookSource: BookSourcePart) { + if (sortAscending) { + viewModel.bottomSource(bookSource) + } else { + viewModel.topSource(bookSource) + } + } + + override fun searchBook(bookSource: BookSourcePart) { + startActivity { + putExtra("searchScope", SearchScope(bookSource).toString()) + } + } + + override fun debug(bookSource: BookSourcePart) { + startActivity { + putExtra("key", bookSource.bookSourceUrl) + } + } + + override fun finish() { + if (searchView.query.isNullOrEmpty()) { + super.finish() + } else { + searchView.setQuery("", true) + } + } + + override fun onDestroy() { + super.onDestroy() + if (!Debug.isChecking) { + Debug.debugMessageMap.clear() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt new file mode 100755 index 000000000..179b038e4 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt @@ -0,0 +1,381 @@ +package io.legado.app.ui.book.source.manage + +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.core.view.doOnLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.databinding.ItemBookSourceBinding +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.model.Debug +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.buildMainHandler +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.startActivity +import io.legado.app.utils.visible +import java.util.Collections + + +class BookSourceAdapter( + context: Context, + private val callBack: CallBack, + private val recyclerView: RecyclerView +) : RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private val selected = linkedSetOf() + private val finalMessageRegex = Regex("成功|失败") + private val handler = buildMainHandler() + var showSourceHost = false + + val selection: List + get() { + return getItems().filter { + selected.contains(it) + } + } + + val diffItemCallback = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { + return oldItem.bookSourceUrl == newItem.bookSourceUrl + } + + override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { + return oldItem.bookSourceName == newItem.bookSourceName + && oldItem.bookSourceGroup == newItem.bookSourceGroup + && oldItem.enabled == newItem.enabled + && oldItem.enabledExplore == newItem.enabledExplore + && oldItem.hasExploreUrl == newItem.hasExploreUrl + } + + override fun getChangePayload(oldItem: BookSourcePart, newItem: BookSourcePart): Any? { + val payload = Bundle() + if (oldItem.bookSourceName != newItem.bookSourceName + || oldItem.bookSourceGroup != newItem.bookSourceGroup + ) { + payload.putBoolean("upName", true) + } + if (oldItem.enabled != newItem.enabled) { + payload.putBoolean("enabled", newItem.enabled) + } + if (oldItem.enabledExplore != newItem.enabledExplore || + oldItem.hasExploreUrl != newItem.hasExploreUrl + ) { + payload.putBoolean("upExplore", true) + } + if (payload.isEmpty) { + return null + } + return payload + } + + } + + override fun getViewBinding(parent: ViewGroup): ItemBookSourceBinding { + return ItemBookSourceBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookSourceBinding, + item: BookSourcePart, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) + cbBookSource.text = item.getDisPlayNameGroup() + swtEnabled.isChecked = item.enabled + cbBookSource.isChecked = selected.contains(item) + upCheckSourceMessage(binding, item) + upShowExplore(ivExplore, item) + upSourceHost(binding, holder.layoutPosition) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "enabled" -> swtEnabled.isChecked = bundle.getBoolean("enabled") + "upName" -> cbBookSource.text = item.getDisPlayNameGroup() + "upExplore" -> upShowExplore(ivExplore, item) + "selected" -> cbBookSource.isChecked = selected.contains(item) + "checkSourceMessage" -> upCheckSourceMessage(binding, item) + "upSourceHost" -> upSourceHost(binding, holder.layoutPosition) + } + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookSourceBinding) { + binding.apply { + swtEnabled.setOnUserCheckedChangeListener { checked -> + getItem(holder.layoutPosition)?.let { + it.enabled = checked + callBack.enable(checked, it) + } + } + cbBookSource.setOnUserCheckedChangeListener { checked -> + getItem(holder.layoutPosition)?.let { + if (checked) { + selected.add(it) + } else { + selected.remove(it) + } + callBack.upCountView() + } + } + ivEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) + } + } + ivMenuMore.setOnClickListener { + showMenu(ivMenuMore, holder.layoutPosition) + } + } + } + + override fun onCurrentListChanged() { + callBack.upCountView() + recyclerView.doOnLayout { + handler.post { + notifyItemRangeChanged(0, itemCount, bundleOf("upSourceHost" to null)) + } + } + } + + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.book_source_item) + popupMenu.menu.findItem(R.id.menu_top).isVisible = callBack.sort == BookSourceSort.Default + popupMenu.menu.findItem(R.id.menu_bottom).isVisible = + callBack.sort == BookSourceSort.Default + val qyMenu = popupMenu.menu.findItem(R.id.menu_enable_explore) + if (!source.hasExploreUrl) { + qyMenu.isVisible = false + } else { + if (source.enabledExplore) { + qyMenu.setTitle(R.string.disable_explore) + } else { + qyMenu.setTitle(R.string.enable_explore) + } + } + val loginMenu = popupMenu.menu.findItem(R.id.menu_login) + loginMenu.isVisible = source.hasLoginUrl + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(source) + R.id.menu_bottom -> callBack.toBottom(source) + R.id.menu_login -> context.startActivity { + putExtra("type", "bookSource") + putExtra("key", source.bookSourceUrl) + } + + R.id.menu_search -> callBack.searchBook(source) + R.id.menu_debug_source -> callBack.debug(source) + R.id.menu_del -> { + callBack.del(source) + selected.remove(source) + } + + R.id.menu_enable_explore -> { + callBack.enableExplore(!source.enabledExplore, source) + } + } + true + } + popupMenu.show() + } + + private fun upShowExplore(iv: ImageView, source: BookSourcePart) { + when { + !source.hasExploreUrl -> { + iv.invisible() + } + + source.enabledExplore -> { + iv.setColorFilter(Color.GREEN) + iv.visible() + iv.contentDescription = context.getString(R.string.tag_explore_enabled) + } + + else -> { + iv.setColorFilter(Color.RED) + iv.visible() + iv.contentDescription = context.getString(R.string.tag_explore_disabled) + } + } + } + + private fun upCheckSourceMessage( + binding: ItemBookSourceBinding, + item: BookSourcePart + ) = binding.run { + val msg = Debug.debugMessageMap[item.bookSourceUrl] ?: "" + ivDebugText.text = msg + val isEmpty = msg.isEmpty() + var isFinalMessage = msg.contains(finalMessageRegex) + if (!Debug.isChecking && !isFinalMessage) { + Debug.updateFinalMessage(item.bookSourceUrl, "校验失败") + ivDebugText.text = Debug.debugMessageMap[item.bookSourceUrl] ?: "" + isFinalMessage = true + } + ivDebugText.visibility = + if (!isEmpty) View.VISIBLE else View.GONE + ivProgressBar.visibility = + if (isFinalMessage || isEmpty || !Debug.isChecking) View.GONE else View.VISIBLE + } + + private fun upSourceHost(binding: ItemBookSourceBinding, position: Int) = binding.run { + if (showSourceHost && isItemHeader(position)) { + tvHostText.text = getHeaderText(position) + tvHostText.visible() + } else { + tvHostText.gone() + } + } + + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun checkSelectedInterval() { + val selectedPosition = linkedSetOf() + getItems().forEachIndexed { index, it -> + if (selected.contains(it)) { + selectedPosition.add(index) + } + } + val minPosition = Collections.min(selectedPosition) + val maxPosition = Collections.max(selectedPosition) + val itemCount = maxPosition - minPosition + 1 + for (i in minPosition..maxPosition) { + getItem(i)?.let { + selected.add(it) + } + } + notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun getHeaderText(position: Int): String { + val source = getItem(position)!! + return callBack.getSourceHost(source.bookSourceUrl) + } + + fun isItemHeader(position: Int): Boolean { + if (position == 0) return true + val lastHost = getHeaderText(position - 1) + val curHost = getHeaderText(position) + return lastHost != curHost + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + val srcOrder = srcItem.customOrder + srcItem.customOrder = targetItem.customOrder + targetItem.customOrder = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = hashSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + val sortNumberSet = hashSetOf() + movedItems.forEach { + sortNumberSet.add(it.customOrder) + } + if (movedItems.size > sortNumberSet.size) { + callBack.upOrder(getItems().mapIndexed { index, bookSourcePart -> + bookSourcePart.customOrder = if (callBack.sortAscending) index else -index + bookSourcePart + }) + } else { + callBack.upOrder(movedItems.toList()) + } + movedItems.clear() + } + } + + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selected + } + + override fun getItemId(position: Int): BookSourcePart { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upCountView() + return true + } + return false + } + } + + interface CallBack { + val sort: BookSourceSort + val sortAscending: Boolean + fun del(bookSource: BookSourcePart) + fun edit(bookSource: BookSourcePart) + fun toTop(bookSource: BookSourcePart) + fun toBottom(bookSource: BookSourcePart) + fun searchBook(bookSource: BookSourcePart) + fun debug(bookSource: BookSourcePart) + fun upOrder(items: List) + fun enable(enable: Boolean, bookSource: BookSourcePart) + fun enableExplore(enable: Boolean, bookSource: BookSourcePart) + fun upCountView() + fun getSourceHost(origin: String): String + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceSort.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceSort.kt new file mode 100755 index 000000000..285ae9a76 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceSort.kt @@ -0,0 +1,5 @@ +package io.legado.app.ui.book.source.manage + +enum class BookSourceSort { + Default, Name, Url, Weight, Update, Enable, Respond +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt new file mode 100755 index 000000000..ea1ceed39 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -0,0 +1,288 @@ +package io.legado.app.ui.book.source.manage + +import android.app.Application +import android.text.TextUtils +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.toBookSource +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.cnCompare +import io.legado.app.utils.outputStream +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeToOutputStream +import splitties.init.appCtx +import java.io.File + +/** + * 书源管理数据修改 + * 修改数据要copy,直接修改会导致界面不刷新 + */ +class BookSourceViewModel(application: Application) : BaseViewModel(application) { + + fun topSource(vararg sources: BookSourcePart) { + execute { + sources.sortBy { it.customOrder } + val minOrder = appDb.bookSourceDao.minOrder - 1 + val array = sources.mapIndexed { index, it -> + it.copy(customOrder = minOrder - index) + } + appDb.bookSourceDao.upOrder(array) + } + } + + fun bottomSource(vararg sources: BookSourcePart) { + execute { + sources.sortBy { it.customOrder } + val maxOrder = appDb.bookSourceDao.maxOrder + 1 + val array = sources.mapIndexed { index, it -> + it.copy(customOrder = maxOrder + index) + } + appDb.bookSourceDao.upOrder(array) + } + } + + fun del(sources: List) { + execute { + SourceHelp.deleteBookSourceParts(sources) + } + } + + fun update(vararg bookSource: BookSource) { + execute { appDb.bookSourceDao.update(*bookSource) } + } + + fun upOrder(items: List) { + if (items.isEmpty()) return + execute { + appDb.bookSourceDao.upOrder(items) + } + } + + fun enable(enable: Boolean, items: List) { + execute { + appDb.bookSourceDao.enable(enable, items) + } + } + + fun enableSelection(sources: List) { + execute { + appDb.bookSourceDao.enable(true, sources) + } + } + + fun disableSelection(sources: List) { + execute { + appDb.bookSourceDao.enable(false, sources) + } + } + + fun enableExplore(enable: Boolean, items: List) { + execute { + appDb.bookSourceDao.enableExplore(enable, items) + } + } + + fun enableSelectExplore(sources: List) { + execute { + appDb.bookSourceDao.enableExplore(true, sources) + } + } + + fun disableSelectExplore(sources: List) { + execute { + appDb.bookSourceDao.enableExplore(false, sources) + } + } + + fun selectionAddToGroups(sources: List, groups: String) { + execute { + val array = sources.map { + it.copy().apply { + addGroup(groups) + } + } + appDb.bookSourceDao.upGroup(array) + } + } + + fun selectionRemoveFromGroups(sources: List, groups: String) { + execute { + val array = sources.map { + it.copy().apply { + removeGroup(groups) + } + } + appDb.bookSourceDao.upGroup(array) + } + } + + private fun saveToFile(sources: List, success: (file: File) -> Unit) { + execute { + val path = "${context.filesDir}/shareBookSource.json" + FileUtils.delete(path) + val file = FileUtils.createFileWithReplace(path) + file.outputStream().buffered().use { + GSON.writeToOutputStream(it, sources) + } + file + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.stackTraceStr) + } + } + + fun saveToFile( + adapter: BookSourceAdapter, + searchKey: String?, + sortAscending: Boolean, + sort: BookSourceSort, + success: (file: File) -> Unit + ) { + execute { + val selection = adapter.selection + val selectedRate = selection.size.toFloat() / adapter.itemCount.toFloat() + val sources = if (selectedRate == 1f) { + getBookSources(searchKey, sortAscending, sort) + } else if (selectedRate < 0.3) { + selection.toBookSource() + } else { + val keys = selection.map { it.bookSourceUrl }.toHashSet() + val bookSources = getBookSources(searchKey, sortAscending, sort) + bookSources.filter { + keys.contains(it.bookSourceUrl) + } + } + saveToFile(sources, success) + } + } + + private fun getBookSources( + searchKey: String?, + sortAscending: Boolean, + sort: BookSourceSort + ): List { + return when { + searchKey.isNullOrEmpty() -> { + appDb.bookSourceDao.all + } + + searchKey == appCtx.getString(R.string.enabled) -> { + appDb.bookSourceDao.allEnabled + } + + searchKey == appCtx.getString(R.string.disabled) -> { + appDb.bookSourceDao.allDisabled + } + + searchKey == appCtx.getString(R.string.need_login) -> { + appDb.bookSourceDao.allLogin + } + + searchKey == appCtx.getString(R.string.no_group) -> { + appDb.bookSourceDao.allNoGroup + } + + searchKey == appCtx.getString(R.string.enabled_explore) -> { + appDb.bookSourceDao.allEnabledExplore + } + + searchKey == appCtx.getString(R.string.disabled_explore) -> { + appDb.bookSourceDao.allDisabledExplore + } + + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.bookSourceDao.groupSearch(key) + } + + else -> { + appDb.bookSourceDao.search(searchKey) + } + }.let { data -> + if (sortAscending) when (sort) { + BookSourceSort.Weight -> data.sortedBy { it.weight } + BookSourceSort.Name -> data.sortedWith { o1, o2 -> + o1.bookSourceName.cnCompare(o2.bookSourceName) + } + + BookSourceSort.Url -> data.sortedBy { it.bookSourceUrl } + BookSourceSort.Update -> data.sortedByDescending { it.lastUpdateTime } + BookSourceSort.Respond -> data.sortedBy { it.respondTime } + BookSourceSort.Enable -> data.sortedWith { o1, o2 -> + var sortNum = -o1.enabled.compareTo(o2.enabled) + if (sortNum == 0) { + sortNum = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sortNum + } + + else -> data + } + else when (sort) { + BookSourceSort.Weight -> data.sortedByDescending { it.weight } + BookSourceSort.Name -> data.sortedWith { o1, o2 -> + o2.bookSourceName.cnCompare(o1.bookSourceName) + } + + BookSourceSort.Url -> data.sortedByDescending { it.bookSourceUrl } + BookSourceSort.Update -> data.sortedBy { it.lastUpdateTime } + BookSourceSort.Respond -> data.sortedByDescending { it.respondTime } + BookSourceSort.Enable -> data.sortedWith { o1, o2 -> + var sortNum = o1.enabled.compareTo(o2.enabled) + if (sortNum == 0) { + sortNum = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sortNum + } + + else -> data.reversed() + } + } + } + + fun addGroup(group: String) { + execute { + val sources = appDb.bookSourceDao.noGroup + sources.forEach { source -> + source.bookSourceGroup = group + } + appDb.bookSourceDao.update(*sources.toTypedArray()) + } + } + + fun upGroup(oldGroup: String, newGroup: String?) { + execute { + val sources = appDb.bookSourceDao.getByGroup(oldGroup) + sources.forEach { source -> + source.bookSourceGroup?.splitNotBlank(",")?.toHashSet()?.let { + it.remove(oldGroup) + if (!newGroup.isNullOrEmpty()) + it.add(newGroup) + source.bookSourceGroup = TextUtils.join(",", it) + } + } + appDb.bookSourceDao.update(*sources.toTypedArray()) + } + } + + fun delGroup(group: String) { + execute { + execute { + val sources = appDb.bookSourceDao.getByGroup(group) + sources.forEach { source -> + source.removeGroup(group) + } + appDb.bookSourceDao.update(*sources.toTypedArray()) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt new file mode 100755 index 000000000..3cabbfacb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt @@ -0,0 +1,138 @@ +package io.legado.app.ui.book.source.manage + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemGroupManageBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.requestInputMethod +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.launch + + +class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val viewModel: BookSourceViewModel by activityViewModels() + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { GroupAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + view.setBackgroundColor(backgroundColor) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = getString(R.string.group_manage) + binding.toolBar.inflateMenu(R.menu.group_manage) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + initData() + } + + private fun initData() { + lifecycleScope.launch { + appDb.bookSourceDao.flowGroups().collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> addGroup() + } + return true + } + + @SuppressLint("InflateParams") + private fun addGroup() { + alert(title = getString(R.string.add_group)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotBlank()) { + viewModel.addGroup(it) + } + } + } + cancelButton() + }.requestInputMethod() + } + + @SuppressLint("InflateParams") + private fun editGroup(group: String) { + alert(title = getString(R.string.group_edit)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setText(group) + } + customView { alertBinding.root } + okButton { + viewModel.upGroup(group, alertBinding.editView.text?.toString()) + } + cancelButton() + }.requestInputMethod() + } + + private inner class GroupAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding { + return ItemGroupManageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupManageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) { + binding.apply { + tvEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + tvDel.setOnClickListener { + getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt new file mode 100755 index 000000000..b9a9fb893 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkAdapter.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.book.toc + +import android.content.Context +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.ItemBookmarkBinding +import io.legado.app.utils.gone +import splitties.views.onLongClick + +class BookmarkAdapter(context: Context, val callback: Callback) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookmarkBinding { + return ItemBookmarkBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookmarkBinding, + item: Bookmark, + payloads: MutableList + ) { + binding.tvChapterName.text = item.chapterName + binding.tvBookText.gone(item.bookText.isEmpty()) + binding.tvBookText.text = item.bookText + binding.tvContent.gone(item.content.isEmpty()) + binding.tvContent.text = item.content + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookmarkBinding) { + binding.root.setOnClickListener { + getItem(holder.layoutPosition)?.let { bookmark -> + callback.onClick(bookmark) + } + } + binding.root.onLongClick { + getItem(holder.layoutPosition)?.let { bookmark -> + callback.onLongClick(bookmark, holder.layoutPosition) + } + } + + } + + interface Callback { + fun onClick(bookmark: Bookmark) + fun onLongClick(bookmark: Bookmark, pos: Int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt new file mode 100755 index 000000000..a386a2ef7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/BookmarkFragment.kt @@ -0,0 +1,96 @@ +package io.legado.app.ui.book.toc + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Bookmark +import io.legado.app.databinding.FragmentBookmarkBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.bookmark.BookmarkDialog +import io.legado.app.ui.widget.recycler.UpLinearLayoutManager +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class BookmarkFragment : VMBaseFragment(R.layout.fragment_bookmark), + BookmarkAdapter.Callback, + TocViewModel.BookmarkCallBack { + override val viewModel by activityViewModels() + private val binding by viewBinding(FragmentBookmarkBinding::bind) + private val mLayoutManager by lazy { UpLinearLayoutManager(requireContext()) } + private val adapter by lazy { BookmarkAdapter(requireContext(), this) } + private var durChapterIndex = 0 + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.bookMarkCallBack = this + initRecyclerView() + viewModel.bookData.observe(this) { + durChapterIndex = it.durChapterIndex + upBookmark(null) + } + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.layoutManager = mLayoutManager + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + } + + override fun upBookmark(searchKey: String?) { + val book = viewModel.bookData.value ?: return + lifecycleScope.launch { + when { + searchKey.isNullOrBlank() -> appDb.bookmarkDao.flowByBook(book.name, book.author) + else -> appDb.bookmarkDao.flowSearch(book.name, book.author, searchKey) + }.catch { + AppLog.put("目录界面获取书签数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + var scrollPos = 0 + withContext(Dispatchers.Default) { + adapter.getItems().forEachIndexed { index, bookmark -> + if (bookmark.chapterIndex >= durChapterIndex) { + return@withContext + } + scrollPos = index + } + } + mLayoutManager.scrollToPositionWithOffset(scrollPos, 0) + } + } + } + + + override fun onClick(bookmark: Bookmark) { + activity?.run { + setResult(Activity.RESULT_OK, Intent().apply { + putExtra("index", bookmark.chapterIndex) + putExtra("chapterPos", bookmark.chapterPos) + }) + finish() + } + } + + override fun onLongClick(bookmark: Bookmark, pos: Int) { + showDialogFragment(BookmarkDialog(bookmark, pos)) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt new file mode 100755 index 000000000..afa2f7d3e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListAdapter.kt @@ -0,0 +1,209 @@ +package io.legado.app.ui.book.toc + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.R +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.ItemChapterListBinding +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.theme.ThemeUtils +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.longToastOnUi +import io.legado.app.utils.visible +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap + +class ChapterListAdapter(context: Context, val callback: Callback) : + DiffRecyclerAdapter(context) { + + val cacheFileNames = hashSetOf() + private val displayTitleMap = ConcurrentHashMap() + private val handler = Handler(Looper.getMainLooper()) + + override val diffItemCallback: DiffUtil.ItemCallback + get() = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: BookChapter, + newItem: BookChapter + ): Boolean { + return oldItem.index == newItem.index + } + + override fun areContentsTheSame( + oldItem: BookChapter, + newItem: BookChapter + ): Boolean { + return oldItem.bookUrl == newItem.bookUrl + && oldItem.url == newItem.url + && oldItem.isVip == newItem.isVip + && oldItem.isPay == newItem.isPay + && oldItem.title == newItem.title + && oldItem.tag == newItem.tag + && oldItem.wordCount == newItem.wordCount + && oldItem.isVolume == newItem.isVolume + } + + } + + private var upDisplayTileJob: Coroutine<*>? = null + + override fun onCurrentListChanged() { + super.onCurrentListChanged() + callback.onListChanged() + } + + fun clearDisplayTitle() { + upDisplayTileJob?.cancel() + displayTitleMap.clear() + } + + fun upDisplayTitles(startIndex: Int) { + upDisplayTileJob?.cancel() + upDisplayTileJob = Coroutine.async(callback.scope) { + val book = callback.book ?: return@async + val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules() + val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule() + val items = getItems() + launch { + for (i in startIndex until items.size) { + val item = items[i] + if (displayTitleMap[item.title] == null) { + ensureActive() + val displayTitle = item.getDisplayTitle(replaceRules, useReplace) + ensureActive() + displayTitleMap[item.title] = displayTitle + handler.post { + notifyItemChanged(i, true) + } + } + } + } + launch { + for (i in startIndex downTo 0) { + val item = items[i] + if (displayTitleMap[item.title] == null) { + ensureActive() + val displayTitle = item.getDisplayTitle(replaceRules, useReplace) + ensureActive() + displayTitleMap[item.title] = displayTitle + handler.post { + notifyItemChanged(i, true) + } + } + } + } + } + } + + private fun getDisplayTitle(chapter: BookChapter): String { + return displayTitleMap[chapter.title] ?: chapter.title + } + + override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding { + return ItemChapterListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemChapterListBinding, + item: BookChapter, + payloads: MutableList + ) { + binding.run { + val isDur = callback.durChapterIndex() == item.index + val cached = callback.isLocalBook + || item.isVolume + || cacheFileNames.contains(item.getFileName()) + if (payloads.isEmpty()) { + if (isDur) { + tvChapterName.setTextColor(context.accentColor) + } else { + tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText)) + } + tvChapterName.text = getDisplayTitle(item) + if (item.isVolume) { + //卷名,如第一卷 突出显示 + tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press)) + } else { + //普通章节 保持不变 + tvChapterItem.background = + ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground) + } + + //卷名不显示 + if (!item.tag.isNullOrEmpty() && !item.isVolume) { + //更新时间规则 + tvTag.text = item.tag + tvTag.visible() + } else { + tvTag.gone() + } + if (AppConfig.tocCountWords && !item.wordCount.isNullOrEmpty() && !item.isVolume) { + //章节字数 + tvWordCount.text = item.wordCount + tvWordCount.visible() + } else { + tvWordCount.gone() + } + + if (item.isVip && !item.isPay) { + ivLocked.visible() + } else { + ivLocked.gone() + } + + upHasCache(binding, isDur, cached) + } else { + tvChapterName.text = getDisplayTitle(item) + upHasCache(binding, isDur, cached) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callback.openChapter(it) + } + } + holder.itemView.setOnLongClickListener { + getItem(holder.layoutPosition)?.let { item -> + context.longToastOnUi(getDisplayTitle(item)) + } + true + } + } + + private fun upHasCache(binding: ItemChapterListBinding, isDur: Boolean, cached: Boolean) = + binding.apply { + ivChecked.setImageResource(R.drawable.ic_outline_cloud_24) + ivChecked.visible(!cached) + if (isDur) { + ivChecked.setImageResource(R.drawable.ic_check) + ivChecked.visible() + } + } + + interface Callback { + val scope: CoroutineScope + val book: Book? + val isLocalBook: Boolean + fun openChapter(bookChapter: BookChapter) + fun durChapterIndex(): Int + fun onListChanged() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt new file mode 100755 index 000000000..9b8c4651c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt @@ -0,0 +1,185 @@ +package io.legado.app.ui.book.toc + +import android.annotation.SuppressLint +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.graphics.PorterDuff +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.databinding.FragmentChapterListBinding +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.ui.widget.recycler.UpLinearLayoutManager +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.observeEvent +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.Default +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapter_list), + ChapterListAdapter.Callback, + TocViewModel.ChapterListCallBack { + override val viewModel by activityViewModels() + private val binding by viewBinding(FragmentChapterListBinding::bind) + private val mLayoutManager by lazy { UpLinearLayoutManager(requireContext()) } + private val adapter by lazy { ChapterListAdapter(requireContext(), this) } + private var durChapterIndex = 0 + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { + viewModel.chapterListCallBack = this@ChapterListFragment + val bbg = bottomBackground + val btc = requireContext().getPrimaryTextColor(ColorUtils.isColorLight(bbg)) + llChapterBaseInfo.setBackgroundColor(bbg) + tvCurrentChapterInfo.setTextColor(btc) + ivChapterTop.setColorFilter(btc, PorterDuff.Mode.SRC_IN) + ivChapterBottom.setColorFilter(btc, PorterDuff.Mode.SRC_IN) + initRecyclerView() + initView() + viewModel.bookData.observe(this@ChapterListFragment) { + initBook(it) + } + } + + private fun initRecyclerView() { + binding.recyclerView.layoutManager = mLayoutManager + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + } + + private fun initView() = binding.run { + ivChapterTop.setOnClickListener { + mLayoutManager.scrollToPositionWithOffset(0, 0) + } + ivChapterBottom.setOnClickListener { + if (adapter.itemCount > 0) { + mLayoutManager.scrollToPositionWithOffset(adapter.itemCount - 1, 0) + } + } + tvCurrentChapterInfo.setOnClickListener { + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + } + binding.llChapterBaseInfo.applyNavigationBarPadding() + } + + @SuppressLint("SetTextI18n") + private fun initBook(book: Book) { + lifecycleScope.launch { + upChapterList(null) + durChapterIndex = book.durChapterIndex + binding.tvCurrentChapterInfo.text = + "${book.durChapterTitle}(${book.durChapterIndex + 1}/${book.simulatedTotalChapterNum()})" + initCacheFileNames(book) + } + } + + private fun initCacheFileNames(book: Book) { + lifecycleScope.launch(IO) { + adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + withContext(Main) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, true) + } + } + } + + override fun observeLiveBus() { + observeEvent>(EventBus.SAVE_CONTENT) { (book, chapter) -> + viewModel.bookData.value?.bookUrl?.let { bookUrl -> + if (book.bookUrl == bookUrl) { + adapter.cacheFileNames.add(chapter.getFileName()) + if (viewModel.searchKey.isNullOrEmpty()) { + adapter.notifyItemChanged(chapter.index, true) + } else { + adapter.getItems().forEachIndexed { index, bookChapter -> + if (bookChapter.index == chapter.index) { + adapter.notifyItemChanged(index, true) + } + } + } + } + } + } + } + + override fun upChapterList(searchKey: String?) { + lifecycleScope.launch { + withContext(IO) { + val end = (book?.simulatedTotalChapterNum() ?: Int.MAX_VALUE) - 1 + when { + searchKey.isNullOrBlank() -> + appDb.bookChapterDao.getChapterList(viewModel.bookUrl, 0, end) + + else -> appDb.bookChapterDao.search(viewModel.bookUrl, searchKey, 0, end) + } + }.let { + adapter.setItems(it) + } + } + } + + override fun onListChanged() { + lifecycleScope.launch { + var scrollPos = 0 + withContext(Default) { + adapter.getItems().forEachIndexed { index, bookChapter -> + if (bookChapter.index >= durChapterIndex) { + return@withContext + } + scrollPos = index + } + } + mLayoutManager.scrollToPositionWithOffset(scrollPos, 0) + adapter.upDisplayTitles(scrollPos) + } + } + + override fun clearDisplayTitle() { + adapter.clearDisplayTitle() + adapter.upDisplayTitles(mLayoutManager.findFirstVisibleItemPosition()) + } + + override fun upAdapter() { + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + + override val scope: CoroutineScope + get() = lifecycleScope + + override val book: Book? + get() = viewModel.bookData.value + + override val isLocalBook: Boolean + get() = viewModel.bookData.value?.isLocal == true + + override fun durChapterIndex(): Int { + return durChapterIndex + } + + override fun openChapter(bookChapter: BookChapter) { + activity?.run { + setResult( + RESULT_OK, Intent() + .putExtra("index", bookChapter.index) + .putExtra("chapterChanged", bookChapter.index != durChapterIndex) + ) + finish() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt new file mode 100755 index 000000000..9b7d88ac3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt @@ -0,0 +1,218 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.ui.book.toc + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentPagerAdapter +import com.google.android.material.tabs.TabLayout +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ActivityChapterListBinding +import io.legado.app.help.book.isLocalTxt +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.model.ReadBook +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.applyTint +import io.legado.app.utils.gone +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible + +/** + * 目录 + */ +class TocActivity : VMBaseActivity(), + TxtTocRuleDialog.CallBack { + + override val binding by viewBinding(ActivityChapterListBinding::inflate) + override val viewModel by viewModels() + + private lateinit var tabLayout: TabLayout + private var menu: Menu? = null + private var searchView: SearchView? = null + private val waitDialog by lazy { WaitDialog(this) } + private val exportDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + when (it.requestCode) { + 1 -> viewModel.saveBookmark(uri) + 2 -> viewModel.saveBookmarkMd(uri) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + tabLayout = binding.titleBar.findViewById(R.id.tab_layout) + tabLayout.isTabIndicatorFullWidth = false + tabLayout.setSelectedTabIndicatorColor(accentColor) + binding.viewPager.adapter = TabFragmentPageAdapter() + tabLayout.setupWithViewPager(binding.viewPager) + tabLayout.tabGravity = TabLayout.GRAVITY_CENTER + viewModel.bookData.observe(this) { + menu?.setGroupVisible(R.id.menu_group_text, it.isLocalTxt) + } + intent.getStringExtra("bookUrl")?.let { + viewModel.initBook(it) + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.book_toc, menu) + this.menu = menu + val search = menu.findItem(R.id.menu_search) + searchView = (search.actionView as SearchView).apply { + applyTint(primaryTextColor) + maxWidth = resources.displayMetrics.widthPixels + onActionViewCollapsed() + setOnCloseListener { + tabLayout.visible() + false + } + setOnSearchClickListener { tabLayout.gone() } + setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + viewModel.searchKey = query + return false + } + + override fun onQueryTextChange(newText: String): Boolean { + viewModel.searchKey = newText + if (tabLayout.selectedTabPosition == 1) { + viewModel.startBookmarkSearch(newText) + } else { + viewModel.startChapterListSearch(newText) + } + return false + } + }) + setOnQueryTextFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + searchView?.isIconified = true + } + } + } + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + if (tabLayout.selectedTabPosition == 1) { + menu.setGroupVisible(R.id.menu_group_bookmark, true) + menu.setGroupVisible(R.id.menu_group_toc, false) + menu.setGroupVisible(R.id.menu_group_text, false) + } else { + menu.setGroupVisible(R.id.menu_group_bookmark, false) + menu.setGroupVisible(R.id.menu_group_toc, true) + menu.setGroupVisible(R.id.menu_group_text, viewModel.bookData.value?.isLocalTxt == true) + } + menu.findItem(R.id.menu_use_replace)?.isChecked = + AppConfig.tocUiUseReplace + menu.findItem(R.id.menu_load_word_count)?.isChecked = + AppConfig.tocCountWords + menu.findItem(R.id.menu_split_long_chapter)?.isChecked = + viewModel.bookData.value?.getSplitLongChapter() == true + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_toc_regex -> showDialogFragment( + TxtTocRuleDialog(viewModel.bookData.value?.tocUrl) + ) + + R.id.menu_split_long_chapter -> { + viewModel.bookData.value?.let { book -> + item.isChecked = !item.isChecked + book.setSplitLongChapter(item.isChecked) + upBookAndToc(book) + } + } + + R.id.menu_reverse_toc -> viewModel.reverseToc { + viewModel.chapterListCallBack?.upChapterList(searchView?.query?.toString()) + setResult(RESULT_OK, Intent().apply { + putExtra("index", it.durChapterIndex) + putExtra("chapterPos", 0) + }) + } + + R.id.menu_use_replace -> { + AppConfig.tocUiUseReplace = !item.isChecked + viewModel.chapterListCallBack?.clearDisplayTitle() + viewModel.chapterListCallBack?.upChapterList(searchView?.query?.toString()) + } + + R.id.menu_load_word_count -> { + AppConfig.tocCountWords = !item.isChecked + viewModel.upChapterListAdapter() + } + + R.id.menu_export_bookmark -> exportDir.launch { + requestCode = 1 + } + + R.id.menu_export_md -> exportDir.launch { + requestCode = 2 + } + + R.id.menu_log -> showDialogFragment() + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onTocRegexDialogResult(tocRegex: String) { + viewModel.bookData.value?.let { book -> + book.tocUrl = tocRegex + upBookAndToc(book) + } + } + + private fun upBookAndToc(book: Book) { + waitDialog.show() + viewModel.upBookTocRule(book) { + waitDialog.dismiss() + if (ReadBook.book == book) { + if (it == null) { + ReadBook.upMsg(null) + } else { + ReadBook.upMsg("LoadTocError:${it.localizedMessage}") + } + } + } + } + + @Suppress("DEPRECATION") + private inner class TabFragmentPageAdapter : + FragmentPagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getItem(position: Int): Fragment { + return when (position) { + 1 -> BookmarkFragment() + else -> ChapterListFragment() + } + } + + override fun getCount(): Int { + return 2 + } + + override fun getPageTitle(position: Int): CharSequence { + return when (position) { + 1 -> getString(R.string.bookmark) + else -> getString(R.string.chapter_list) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt new file mode 100755 index 000000000..8cd5d47b0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocActivityResult.kt @@ -0,0 +1,27 @@ +package io.legado.app.ui.book.toc + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract + +class TocActivityResult : ActivityResultContract?>() { + + override fun createIntent(context: Context, input: String): Intent { + return Intent(context, TocActivity::class.java) + .putExtra("bookUrl", input) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Triple? { + if (resultCode == RESULT_OK) { + intent?.let { + return Triple( + it.getIntExtra("index", 0), + it.getIntExtra("chapterPos", 0), + it.getBooleanExtra("chapterChanged", false) + ) + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt new file mode 100755 index 000000000..5ca389bbb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt @@ -0,0 +1,136 @@ +package io.legado.app.ui.book.toc + + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.ReadBook +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.FileDoc +import io.legado.app.utils.GSON +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeText + +class TocViewModel(application: Application) : BaseViewModel(application) { + var bookUrl: String = "" + var bookData = MutableLiveData() + var chapterListCallBack: ChapterListCallBack? = null + var bookMarkCallBack: BookmarkCallBack? = null + var searchKey: String? = null + + fun initBook(bookUrl: String) { + this.bookUrl = bookUrl + execute { + appDb.bookDao.getBook(bookUrl)?.let { + bookData.postValue(it) + } + } + } + + fun upBookTocRule(book: Book, complete: (Throwable?) -> Unit) { + execute { + appDb.bookDao.update(book) + LocalBook.getChapterList(book).let { + appDb.bookChapterDao.delByBook(book.bookUrl) + appDb.bookChapterDao.insert(*it.toTypedArray()) + appDb.bookDao.update(book) + ReadBook.onChapterListUpdated(book) + bookData.postValue(book) + } + }.onSuccess { + complete.invoke(null) + }.onError { + complete.invoke(it) + } + } + + fun reverseToc(success: (book: Book) -> Unit) { + execute { + bookData.value?.apply { + setReverseToc(!getReverseToc()) + val toc = appDb.bookChapterDao.getChapterList(bookUrl) + val newToc = toc.reversed() + newToc.forEachIndexed { index, bookChapter -> + bookChapter.index = index + } + appDb.bookChapterDao.insert(*newToc.toTypedArray()) + } + }.onSuccess { + it?.let(success) + } + } + + fun startChapterListSearch(newText: String?) { + chapterListCallBack?.upChapterList(newText) + } + + fun startBookmarkSearch(newText: String?) { + bookMarkCallBack?.upBookmark(newText) + } + + fun upChapterListAdapter() { + chapterListCallBack?.upAdapter() + } + + fun saveBookmark(treeUri: Uri) { + execute { + val book = bookData.value + ?: throw NoStackTraceException(context.getString(R.string.no_book)) + val fileName = "bookmark-${book.name} ${book.author}.json" + val doc = FileDoc.fromUri(treeUri, true) + doc.createFileIfNotExist(fileName).writeText( + GSON.toJson( + appDb.bookmarkDao.getByBook(book.name, book.author) + ) + ) + }.onError { + AppLog.put("导出失败\n${it.localizedMessage}", it, true) + }.onSuccess { + context.toastOnUi("导出成功") + } + } + + fun saveBookmarkMd(treeUri: Uri) { + execute { + val book = bookData.value + ?: throw NoStackTraceException(context.getString(R.string.no_book)) + val fileName = "bookmark-${book.name} ${book.author}.md" + val treeDoc = FileDoc.fromUri(treeUri, true) + val fileDoc = treeDoc.createFileIfNotExist(fileName) + .openOutputStream() + .getOrThrow() + fileDoc.use { outputStream -> + outputStream.write("## ${book.name} ${book.author}\n\n".toByteArray()) + appDb.bookmarkDao.getByBook(book.name, book.author).forEach { + outputStream.write("#### ${it.chapterName}\n\n".toByteArray()) + outputStream.write("###### 原文\n ${it.bookText}\n\n".toByteArray()) + outputStream.write("###### 摘要\n ${it.content}\n\n".toByteArray()) + } + } + }.onError { + AppLog.put("导出失败\n${it.localizedMessage}", it, true) + }.onSuccess { + context.toastOnUi("导出成功") + } + } + + interface ChapterListCallBack { + fun upChapterList(searchKey: String?) + + fun clearDisplayTitle() + + fun upAdapter() + } + + interface BookmarkCallBack { + fun upBookmark(searchKey: String?) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt new file mode 100755 index 000000000..d759b761d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleActivity.kt @@ -0,0 +1,263 @@ +package io.legado.app.ui.book.toc.rule + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.databinding.ActivityTxtTocRuleBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.association.ImportTxtTocRuleDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ACache +import io.legado.app.utils.GSON +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.launch +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +class TxtTocRuleActivity : VMBaseActivity(), + TxtTocRuleAdapter.CallBack, + SelectActionBar.CallBack, + TxtTocRuleEditDialog.Callback, + PopupMenu.OnMenuItemClickListener { + + override val viewModel by viewModels() + override val binding by viewBinding(ActivityTxtTocRuleBinding::inflate) + private val adapter: TxtTocRuleAdapter by lazy { + TxtTocRuleAdapter(this, this) + } + private val importTocRuleKey = "tocRuleUrl" + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + showDialogFragment(ImportTxtTocRuleDialog(it)) + } + private val importDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportTxtTocRuleDialog(uri.toString())) + } + } + private val exportResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + sendToClip(uri.toString()) + } + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initBottomActionBar() + initData() + } + + private fun initView() = binding.run { + recyclerView.setEdgeEffectColor(primaryColor) + recyclerView.addItemDecoration(VerticalDivider(this@TxtTocRuleActivity)) + recyclerView.adapter = adapter + // When this page is opened, it is in selection mode + val dragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + dragSelectTouchHelper.activeSlideSelect() + // Note: need judge selection first, so add ItemTouchHelper after it. + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initBottomActionBar() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.txt_toc_rule_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun initData() { + lifecycleScope.launch { + appDb.txtTocRuleDao.observeAll().catch { + AppLog.put("TXT目录规则界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { tocRules -> + adapter.setItems(tocRules, adapter.diffItemCallBack) + } + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.txt_toc_rule, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add -> showDialogFragment(TxtTocRuleEditDialog()) + R.id.menu_import_local -> importDoc.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_qr -> qrCodeResult.launch() + R.id.menu_import_default -> viewModel.importDefault() + R.id.menu_help -> showHelp("txtTocRuleHelp") + + } + return super.onCompatOptionsItemSelected(item) + } + + override fun del(source: TxtTocRule) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + source.name) + noButton() + yesButton { + viewModel.del(source) + } + } + } + + override fun edit(source: TxtTocRule) { + showDialogFragment(TxtTocRuleEditDialog(source.id)) + } + + override fun onClickSelectBarMainAction() { + delSourceDialog() + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun saveTxtTocRule(txtTocRule: TxtTocRule) { + viewModel.save(txtTocRule) + } + + override fun update(vararg source: TxtTocRule) { + viewModel.update(*source) + } + + override fun toTop(source: TxtTocRule) { + viewModel.toTop(source) + } + + override fun toBottom(source: TxtTocRule) { + viewModel.toBottom(source) + } + + override fun upOrder() { + viewModel.upOrder() + } + + override fun upCountView() { + binding.selectActionBar + .upCountView(adapter.selection.size, adapter.itemCount) + } + + private fun delSourceDialog() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + yesButton { viewModel.del(*adapter.selection.toTypedArray()) } + noButton() + } + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(cacheDir = false) + val defaultUrl = "https://gitee.com/fisher52/YueDuJson/raw/master/myTxtChapterRule.json" + val cacheUrls: MutableList = aCache + .getAsString(importTocRuleKey) + ?.splitNotBlank(",") + ?.toMutableList() + ?: mutableListOf() + if (!cacheUrls.contains(defaultUrl)) { + cacheUrls.add(0, defaultUrl) + } + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (it.isAbsUrl() && !cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) + } + showDialogFragment(ImportTxtTocRuleDialog(it)) + } + } + cancelButton() + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection( + *adapter.selection.toTypedArray() + ) + + R.id.menu_disable_selection -> viewModel.disableSelection( + *adapter.selection.toTypedArray() + ) + + R.id.menu_export_selection -> exportResult.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "exportTxtTocRule.json", + GSON.toJson(adapter.selection).toByteArray(), + "application/json" + ) + } + } + return true + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleAdapter.kt new file mode 100755 index 000000000..e7a54251c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleAdapter.kt @@ -0,0 +1,233 @@ +package io.legado.app.ui.book.toc.rule + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.databinding.ItemTxtTocRuleBinding +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.ColorUtils + +class TxtTocRuleAdapter(context: Context, private val callBack: CallBack) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private val selected = linkedSetOf() + + val selection: List + get() = getItems().filter { + selected.contains(it) + } + + val diffItemCallBack = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: TxtTocRule, newItem: TxtTocRule): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: TxtTocRule, newItem: TxtTocRule): Boolean { + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.enable != newItem.enable) { + return false + } + if (oldItem.example != newItem.example) { + return false + } + return true + } + + override fun getChangePayload(oldItem: TxtTocRule, newItem: TxtTocRule): Any? { + val payload = Bundle() + if (oldItem.name != newItem.name) { + payload.putBoolean("upName", true) + } + if (oldItem.enable != newItem.enable) { + payload.putBoolean("enabled", newItem.enable) + } + if (oldItem.example != newItem.example) { + payload.putBoolean("upExample", true) + } + if (payload.isEmpty) { + return null + } + return payload + } + } + + override fun getViewBinding(parent: ViewGroup): ItemTxtTocRuleBinding { + return ItemTxtTocRuleBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemTxtTocRuleBinding, + item: TxtTocRule, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) + cbSource.text = item.name + swtEnabled.isChecked = item.enable + cbSource.isChecked = selected.contains(item) + titleExample.text = item.example + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "selected" -> cbSource.isChecked = selected.contains(item) + "upName" -> cbSource.text = item.name + "upExample" -> titleExample.text = item.example + "enabled" -> swtEnabled.isChecked = item.enable + } + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemTxtTocRuleBinding) { + binding.cbSource.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + if (isChecked) { + selected.add(it) + } else { + selected.remove(it) + } + callBack.upCountView() + } + } + binding.swtEnabled.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + it.enable = isChecked + callBack.update(it) + } + } + binding.ivEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) + } + } + binding.ivMenuMore.setOnClickListener { + showMenu(it, holder.layoutPosition) + } + } + + override fun onCurrentListChanged() { + callBack.upCountView() + } + + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.txt_toc_rule_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(source) + R.id.menu_bottom -> callBack.toBottom(source) + R.id.menu_del -> { + callBack.del(source) + selected.remove(source) + } + } + true + } + popupMenu.show() + } + + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.serialNumber == targetItem.serialNumber) { + callBack.upOrder() + } else { + val srcOrder = srcItem.serialNumber + srcItem.serialNumber = targetItem.serialNumber + targetItem.serialNumber = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = hashSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() + } + } + + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selected + } + + override fun getItemId(position: Int): TxtTocRule { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upCountView() + return true + } + return false + } + } + + interface CallBack { + fun del(source: TxtTocRule) + fun edit(source: TxtTocRule) + fun update(vararg source: TxtTocRule) + fun toTop(source: TxtTocRule) + fun toBottom(source: TxtTocRule) + fun upOrder() + fun upCountView() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleDialog.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleDialog.kt new file mode 100755 index 000000000..4a44d23ad --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleDialog.kt @@ -0,0 +1,325 @@ +package io.legado.app.ui.book.toc.rule + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogTocRegexBinding +import io.legado.app.databinding.ItemTocRegexBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.association.ImportTxtTocRuleDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ACache +import io.legado.app.utils.applyTint +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.launch +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * txt目录规则 + */ +class TxtTocRuleDialog() : BaseDialogFragment(R.layout.dialog_toc_regex), + Toolbar.OnMenuItemClickListener, + TxtTocRuleEditDialog.Callback { + + constructor(tocRegex: String?) : this() { + arguments = Bundle().apply { + putString("tocRegex", tocRegex) + } + } + + private val importTocRuleKey = "tocRuleUrl" + private val viewModel: TxtTocRuleViewModel by viewModels() + private val binding by viewBinding(DialogTocRegexBinding::bind) + private val adapter by lazy { TocRegexAdapter(requireContext()) } + var selectedName: String? = null + private var durRegex: String? = null + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + showDialogFragment(ImportTxtTocRuleDialog(it)) + } + private val importDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportTxtTocRuleDialog(uri.toString())) + } + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.8f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + durRegex = arguments?.getString("tocRegex") + binding.toolBar.setTitle(R.string.txt_toc_rule) + binding.toolBar.inflateMenu(R.menu.txt_toc_rule) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + initView() + initData() + } + + private fun initView() = binding.run { + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recyclerView) + tvCancel.setOnClickListener { + dismissAllowingStateLoss() + } + tvOk.setOnClickListener { + adapter.getItems().forEach { tocRule -> + if (selectedName == tocRule.name) { + val callBack = activity as? CallBack + callBack?.onTocRegexDialogResult(tocRule.rule) + dismissAllowingStateLoss() + return@setOnClickListener + } + } + } + } + + private fun initData() { + lifecycleScope.launch { + appDb.txtTocRuleDao.observeAll().catch { + AppLog.put("TXT目录规则对话框获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { tocRules -> + initSelectedName(tocRules) + adapter.setItems(tocRules, adapter.diffItemCallBack) + } + } + } + + private fun initSelectedName(tocRules: List) { + if (selectedName == null && durRegex != null) { + tocRules.forEach { + if (durRegex == it.rule) { + selectedName = it.name + return@forEach + } + } + if (selectedName == null) { + selectedName = "" + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> showDialogFragment(TxtTocRuleEditDialog()) + R.id.menu_import_local -> importDoc.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_qr -> qrCodeResult.launch() + R.id.menu_import_default -> viewModel.importDefault() + R.id.menu_help -> showHelp("txtTocRuleHelp") + } + return false + } + + override fun saveTxtTocRule(txtTocRule: TxtTocRule) { + viewModel.save(txtTocRule) + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(cacheDir = false) + val defaultUrl = "https://gitee.com/fisher52/YueDuJson/raw/master/myTxtChapterRule.json" + val cacheUrls: MutableList = aCache + .getAsString(importTocRuleKey) + ?.splitNotBlank(",") + ?.toMutableList() + ?: mutableListOf() + if (!cacheUrls.contains(defaultUrl)) { + cacheUrls.add(0, defaultUrl) + } + requireContext().alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (it.isAbsUrl() && !cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importTocRuleKey, cacheUrls.joinToString(",")) + } + showDialogFragment(ImportTxtTocRuleDialog(it)) + } + } + cancelButton() + } + } + + inner class TocRegexAdapter(context: Context) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + val diffItemCallBack = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: TxtTocRule, newItem: TxtTocRule): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: TxtTocRule, newItem: TxtTocRule): Boolean { + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.enable != newItem.enable) { + return false + } + if (oldItem.example != newItem.example) { + return false + } + return true + } + + override fun getChangePayload(oldItem: TxtTocRule, newItem: TxtTocRule): Any? { + val payload = Bundle() + if (oldItem.name != newItem.name) { + payload.putBoolean("upName", true) + } + if (oldItem.enable != newItem.enable) { + payload.putBoolean("enabled", newItem.enable) + } + if (oldItem.example != newItem.example) { + payload.putBoolean("upExample", true) + } + if (payload.isEmpty) { + return null + } + return payload + } + } + + override fun getViewBinding(parent: ViewGroup): ItemTocRegexBinding { + return ItemTocRegexBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemTocRegexBinding, + item: TxtTocRule, + payloads: MutableList + ) { + binding.apply { + if (payloads.isEmpty()) { + root.setBackgroundColor(context.backgroundColor) + rbRegexName.text = item.name + titleExample.text = item.example + rbRegexName.isChecked = item.name == selectedName + swtEnabled.isChecked = item.enable + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "upName" -> rbRegexName.text = item.name + "upExample" -> titleExample.text = item.example + "enabled" -> swtEnabled.isChecked = item.enable + "upSelect" -> rbRegexName.isChecked = item.name == selectedName + } + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemTocRegexBinding) { + binding.apply { + rbRegexName.setOnUserCheckedChangeListener { isChecked -> + if (isChecked) { + selectedName = getItem(holder.layoutPosition)?.name + updateItems(0, itemCount - 1, bundleOf("upSelect" to null)) + } + } + swtEnabled.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + it.enable = isChecked + viewModel.update(it) + } + } + ivEdit.setOnClickListener { + showDialogFragment(TxtTocRuleEditDialog(getItem(holder.layoutPosition)?.id)) + } + ivDelete.setOnClickListener { + getItem(holder.layoutPosition)?.let { item -> + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + item.name) + noButton() + yesButton { + viewModel.del(item) + } + } + } + } + } + } + + private var isMoved = false + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) + isMoved = true + return super.swap(srcPosition, targetPosition) + } + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.onClearView(recyclerView, viewHolder) + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.serialNumber = index + 1 + } + viewModel.update(*getItems().toTypedArray()) + } + isMoved = false + } + } + + interface CallBack { + fun onTocRegexDialogResult(tocRegex: String) {} + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleEditDialog.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleEditDialog.kt new file mode 100755 index 000000000..4d8409a74 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleEditDialog.kt @@ -0,0 +1,147 @@ +package io.legado.app.ui.book.toc.rule + +import android.app.Application +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.databinding.DialogTocRegexEditBinding +import io.legado.app.exception.NoStackTraceException +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException + +class TxtTocRuleEditDialog() : BaseDialogFragment(R.layout.dialog_toc_regex_edit, true), + Toolbar.OnMenuItemClickListener { + + constructor(id: Long?) : this() { + id ?: return + arguments = Bundle().apply { + putLong("id", id) + } + } + + private val binding by viewBinding(DialogTocRegexEditBinding::bind) + private val viewModel by viewModels() + private val callback get() = (parentFragment as? Callback) ?: activity as? Callback + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + initMenu() + viewModel.initData(arguments?.getLong("id")) { + upRuleView(it) + } + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.txt_toc_rule_edit) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_save -> { + val tocRule = getRuleFromView() + if (checkValid(tocRule)) { + callback?.saveTxtTocRule(getRuleFromView()) + dismissAllowingStateLoss() + } + } + R.id.menu_copy_rule -> context?.sendToClip(GSON.toJson(getRuleFromView())) + R.id.menu_paste_rule -> viewModel.pasteRule { + upRuleView(it) + } + } + return true + } + + private fun checkValid(tocRule: TxtTocRule): Boolean { + if (tocRule.name.isEmpty()) { + toastOnUi("名称不能为空") + return false + } + + try { + Pattern.compile(tocRule.rule, Pattern.MULTILINE) + } catch (ex: PatternSyntaxException) { + AppLog.put("正则语法错误或不支持(txt):${ex.localizedMessage}", ex, true) + return false + } + + return true + } + + private fun upRuleView(tocRule: TxtTocRule?) { + binding.tvRuleName.setText(tocRule?.name) + binding.tvRuleRegex.setText(tocRule?.rule) + binding.tvRuleExample.setText(tocRule?.example) + } + + private fun getRuleFromView(): TxtTocRule { + val tocRule = viewModel.tocRule ?: TxtTocRule().apply { + viewModel.tocRule = this + } + binding.run { + tocRule.name = tvRuleName.text.toString() + tocRule.rule = tvRuleRegex.text.toString() + tocRule.example = tvRuleExample.text.toString() + } + return tocRule + } + + class ViewModel(application: Application) : BaseViewModel(application) { + + var tocRule: TxtTocRule? = null + + fun initData(id: Long?, finally: (tocRule: TxtTocRule?) -> Unit) { + if (tocRule != null) return + execute { + if (id == null) return@execute + tocRule = appDb.txtTocRuleDao.get(id) + }.onFinally { + finally.invoke(tocRule) + } + } + + fun pasteRule(success: (TxtTocRule) -> Unit) { + execute(context = Dispatchers.Main) { + val text = context.getClipText() + if (text.isNullOrBlank()) { + throw NoStackTraceException("剪贴板为空") + } + GSON.fromJsonObject(text).getOrNull() + ?: throw NoStackTraceException("格式不对") + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage ?: "Error") + it.printOnDebug() + } + } + + } + + interface Callback { + + fun saveTxtTocRule(txtTocRule: TxtTocRule) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt new file mode 100755 index 000000000..fd3597fde --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt @@ -0,0 +1,79 @@ +package io.legado.app.ui.book.toc.rule + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.DefaultData + +class TxtTocRuleViewModel(app: Application) : BaseViewModel(app) { + + fun save(txtTocRule: TxtTocRule) { + execute { + appDb.txtTocRuleDao.insert(txtTocRule) + } + } + + fun del(vararg txtTocRule: TxtTocRule) { + execute { + appDb.txtTocRuleDao.delete(*txtTocRule) + } + } + + fun update(vararg txtTocRule: TxtTocRule) { + execute { + appDb.txtTocRuleDao.update(*txtTocRule) + } + } + + fun importDefault() { + execute { + DefaultData.importDefaultTocRules() + } + } + + fun toTop(vararg rules: TxtTocRule) { + execute { + val minOrder = appDb.txtTocRuleDao.minOrder - 1 + rules.forEachIndexed { index, source -> + source.serialNumber = minOrder - index + } + appDb.txtTocRuleDao.update(*rules) + } + } + + fun toBottom(vararg sources: TxtTocRule) { + execute { + val maxOrder = appDb.txtTocRuleDao.maxOrder + 1 + sources.forEachIndexed { index, source -> + source.serialNumber = maxOrder + index + } + appDb.txtTocRuleDao.update(*sources) + } + } + + fun upOrder() { + execute { + val sources = appDb.txtTocRuleDao.all + for ((index: Int, source: TxtTocRule) in sources.withIndex()) { + source.serialNumber = index + 1 + } + appDb.txtTocRuleDao.update(*sources.toTypedArray()) + } + } + + fun enableSelection(vararg txtTocRule: TxtTocRule) { + execute { + val array = txtTocRule.map { it.copy(enable = true) }.toTypedArray() + appDb.txtTocRuleDao.insert(*array) + } + } + + fun disableSelection(vararg txtTocRule: TxtTocRule) { + execute { + val array = txtTocRule.map { it.copy(enable = false) }.toTypedArray() + appDb.txtTocRuleDao.insert(*array) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt b/app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt new file mode 100755 index 000000000..a271f4ae7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt @@ -0,0 +1,345 @@ +package io.legado.app.ui.browser + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.net.Uri +import android.net.http.SslError +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.webkit.CookieManager +import android.webkit.SslErrorHandler +import android.webkit.URLUtil +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.core.view.size +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.imagePathKey +import io.legado.app.databinding.ActivityWebViewBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.http.CookieStore +import io.legado.app.help.source.SourceVerificationHelp +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.model.Download +import io.legado.app.ui.association.OnLineImportActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.ACache +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.keepScreenOn +import io.legado.app.utils.longSnackbar +import io.legado.app.utils.openUrl +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setDarkeningAllowed +import io.legado.app.utils.startActivity +import io.legado.app.utils.toggleSystemBar +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import java.net.URLDecoder +import io.legado.app.help.http.CookieManager as AppCookieManager + +class WebViewActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivityWebViewBinding::inflate) + override val viewModel by viewModels() + private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null + private var webPic: String? = null + private var isCloudflareChallenge = false + private var isFullScreen = false + private val saveImage = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + ACache.get().put(imagePathKey, uri.toString()) + viewModel.saveImage(webPic, uri.toString()) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.titleBar.title = intent.getStringExtra("title") ?: getString(R.string.loading) + binding.titleBar.subtitle = intent.getStringExtra("sourceName") + viewModel.initData(intent) { + val url = viewModel.baseUrl + val headerMap = viewModel.headerMap + initWebView(url, headerMap) + val html = viewModel.html + if (html.isNullOrEmpty()) { + binding.webView.loadUrl(url, headerMap) + } else { + binding.webView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url) + } + } + onBackPressedDispatcher.addCallback(this) { + if (binding.customWebView.size > 0) { + customWebViewCallback?.onCustomViewHidden() + return@addCallback + } else if (binding.webView.canGoBack() + && binding.webView.copyBackForwardList().size > 1 + ) { + binding.webView.goBack() + return@addCallback + } + if (isFullScreen) { + toggleFullScreen() + return@addCallback + } + finish() + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.web_view, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + if (viewModel.sourceOrigin.isNotEmpty()) { + menu.findItem(R.id.menu_disable_source)?.isVisible = true + menu.findItem(R.id.menu_delete_source)?.isVisible = true + } + return super.onPrepareOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_open_in_browser -> openUrl(viewModel.baseUrl) + R.id.menu_copy_url -> sendToClip(viewModel.baseUrl) + R.id.menu_ok -> { + if (viewModel.sourceVerificationEnable) { + viewModel.saveVerificationResult(binding.webView) { + finish() + } + } else { + finish() + } + } + + R.id.menu_full_screen -> toggleFullScreen() + R.id.menu_disable_source -> { + viewModel.disableSource { + finish() + } + } + + R.id.menu_delete_source -> { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + viewModel.sourceName) + noButton() + yesButton { + viewModel.deleteSource { + finish() + } + } + } + } + } + return super.onCompatOptionsItemSelected(item) + } + + //实现starBrowser调起页面全屏 + private fun toggleFullScreen() { + isFullScreen = !isFullScreen + + toggleSystemBar(!isFullScreen) + + if (isFullScreen) { + supportActionBar?.hide() + } else { + supportActionBar?.show() + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun initWebView(url: String, headerMap: HashMap) { + binding.progressBar.fontColor = accentColor + binding.webView.webChromeClient = CustomWebChromeClient() + binding.webView.webViewClient = CustomWebViewClient() + binding.webView.settings.apply { + setDarkeningAllowed(AppConfig.isNightTheme) + mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + domStorageEnabled = true + allowContentAccess = true + useWideViewPort = true + loadWithOverviewMode = true + javaScriptEnabled = true + builtInZoomControls = true + displayZoomControls = false + headerMap[AppConst.UA_NAME]?.let { + userAgentString = it + } + } + AppCookieManager.applyToWebView(url) + binding.webView.setOnLongClickListener { + val hitTestResult = binding.webView.hitTestResult + if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || + hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE + ) { + hitTestResult.extra?.let { webPic -> + selector( + arrayListOf( + SelectItem(getString(R.string.action_save), "save"), + SelectItem(getString(R.string.select_folder), "selectFolder") + ) + ) { _, charSequence, _ -> + when (charSequence.value) { + "save" -> saveImage(webPic) + "selectFolder" -> selectSaveFolder() + } + } + return@setOnLongClickListener true + } + } + return@setOnLongClickListener false + } + binding.webView.setDownloadListener { downloadUrl, _, contentDisposition, _, _ -> + var fileName = URLUtil.guessFileName(downloadUrl, contentDisposition, null) + fileName = URLDecoder.decode(fileName, "UTF-8") + binding.llView.longSnackbar(fileName, getString(R.string.action_download)) { + Download.start(this, downloadUrl, fileName) + } + } + } + + private fun saveImage(webPic: String) { + this.webPic = webPic + val path = ACache.get().getAsString(imagePathKey) + if (path.isNullOrEmpty()) { + selectSaveFolder() + } else { + viewModel.saveImage(webPic, path) + } + } + + private fun selectSaveFolder() { + val default = arrayListOf>() + val path = ACache.get().getAsString(imagePathKey) + if (!path.isNullOrEmpty()) { + default.add(SelectItem(path, -1)) + } + saveImage.launch { + otherActions = default + } + } + + override fun finish() { + SourceVerificationHelp.checkResult(viewModel.sourceOrigin) + super.finish() + } + + override fun onDestroy() { + super.onDestroy() + binding.webView.destroy() + } + + inner class CustomWebChromeClient : WebChromeClient() { + + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + binding.progressBar.setDurProgress(newProgress) + binding.progressBar.gone(newProgress == 100) + } + + override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR + binding.llView.invisible() + binding.customWebView.addView(view) + customWebViewCallback = callback + keepScreenOn(true) + toggleSystemBar(false) + } + + override fun onHideCustomView() { + binding.customWebView.removeAllViews() + binding.llView.visible() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + keepScreenOn(false) + toggleSystemBar(true) + } + } + + inner class CustomWebViewClient : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + request?.let { + return shouldOverrideUrlLoading(it.url) + } + return true + } + + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "KotlinRedundantDiagnosticSuppress") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + url?.let { + return shouldOverrideUrlLoading(Uri.parse(it)) + } + return true + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + val cookieManager = CookieManager.getInstance() + url?.let { + CookieStore.setCookie(it, cookieManager.getCookie(it)) + } + view?.title?.let { title -> + if (title != url && title != view.url && title.isNotBlank()) { + binding.titleBar.title = title + } else { + binding.titleBar.title = intent.getStringExtra("title") + } + view.evaluateJavascript("!!window._cf_chl_opt") { + if (it == "true") { + isCloudflareChallenge = true + } else if (isCloudflareChallenge && viewModel.sourceVerificationEnable) { + viewModel.saveVerificationResult(binding.webView) { + finish() + } + } + } + } + } + + private fun shouldOverrideUrlLoading(url: Uri): Boolean { + when (url.scheme) { + "http", "https" -> { + return false + } + + "legado", "yuedu" -> { + startActivity { + data = url + } + return true + } + + else -> { + binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) { + openUrl(url) + } + return true + } + } + } + + @SuppressLint("WebViewClientOnReceivedSslError") + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + handler?.proceed() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/browser/WebViewModel.kt b/app/src/main/java/io/legado/app/ui/browser/WebViewModel.kt new file mode 100755 index 000000000..202707c05 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/browser/WebViewModel.kt @@ -0,0 +1,142 @@ +package io.legado.app.ui.browser + +import android.app.Application +import android.content.Intent +import android.util.Base64 +import android.webkit.URLUtil +import android.webkit.WebView +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.imagePathKey +import io.legado.app.constant.SourceType +import io.legado.app.data.appDb +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.source.SourceHelp +import io.legado.app.help.source.SourceVerificationHelp +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.ACache +import io.legado.app.utils.FileDoc +import io.legado.app.utils.createFileIfNotExist +import io.legado.app.utils.openOutputStream +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.toastOnUi +import org.apache.commons.text.StringEscapeUtils +import java.util.Date + +class WebViewModel(application: Application) : BaseViewModel(application) { + var intent: Intent? = null + var baseUrl: String = "" + var html: String? = null + val headerMap: HashMap = hashMapOf() + var sourceVerificationEnable: Boolean = false + var refetchAfterSuccess: Boolean = true + var sourceName: String = "" + var sourceOrigin: String = "" + var sourceType = SourceType.book + + fun initData( + intent: Intent, + success: () -> Unit + ) { + execute { + this@WebViewModel.intent = intent + val url = intent.getStringExtra("url") + ?: throw NoStackTraceException("url不能为空") + sourceName = intent.getStringExtra("sourceName") ?: "" + sourceOrigin = intent.getStringExtra("sourceOrigin") ?: "" + sourceType = intent.getIntExtra("sourceType", SourceType.book) + sourceVerificationEnable = intent.getBooleanExtra("sourceVerificationEnable", false) + refetchAfterSuccess = intent.getBooleanExtra("refetchAfterSuccess", true) + val source = SourceHelp.getSource(sourceOrigin, sourceType) + val analyzeUrl = AnalyzeUrl(url, source = source, coroutineContext = coroutineContext) + baseUrl = analyzeUrl.url + headerMap.putAll(analyzeUrl.headerMap) + if (analyzeUrl.isPost()) { + html = analyzeUrl.getStrResponseAwait(useWebView = false).body + } + }.onSuccess { + success.invoke() + }.onError { + context.toastOnUi("error\n${it.localizedMessage}") + it.printOnDebug() + } + } + + fun saveImage(webPic: String?, path: String) { + webPic ?: return + execute { + val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg" + webData2bitmap(webPic)?.let { byteArray -> + val fileDoc = FileDoc.fromDir(path) + val picFile = fileDoc.createFileIfNotExist(fileName) + picFile.openOutputStream().getOrThrow().use { + it.write(byteArray) + } + } ?: throw Throwable("NULL") + }.onError { + ACache.get().remove(imagePathKey) + context.toastOnUi("保存图片失败:${it.localizedMessage}") + }.onSuccess { + context.toastOnUi("保存成功") + } + } + + private suspend fun webData2bitmap(data: String): ByteArray? { + return if (URLUtil.isValidUrl(data)) { + okHttpClient.newCallResponseBody { + url(data) + }.bytes() + } else { + Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT) + } + } + + fun saveVerificationResult(webView: WebView, success: () -> Unit) { + if (!sourceVerificationEnable) { + return success.invoke() + } + if (refetchAfterSuccess) { + execute { + val url = intent!!.getStringExtra("url")!! + val source = appDb.bookSourceDao.getBookSource(sourceOrigin) + html = AnalyzeUrl( + url, + headerMapF = headerMap, + source = source, + coroutineContext = coroutineContext + ).getStrResponseAwait(useWebView = false).body + SourceVerificationHelp.setResult(sourceOrigin, html ?: "") + }.onSuccess { + success.invoke() + } + } else { + webView.evaluateJavascript("document.documentElement.outerHTML") { + execute { + html = StringEscapeUtils.unescapeJson(it).trim('"') + SourceVerificationHelp.setResult(sourceOrigin, html ?: "") + }.onSuccess { + success.invoke() + } + } + } + } + + fun disableSource(block: () -> Unit) { + execute { + SourceHelp.enableSource(sourceOrigin, sourceType, false) + }.onSuccess { + block.invoke() + } + } + + fun deleteSource(block: () -> Unit) { + execute { + SourceHelp.deleteSource(sourceOrigin, sourceType) + }.onSuccess { + block.invoke() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt new file mode 100755 index 000000000..4b81f9f94 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt @@ -0,0 +1,400 @@ +package io.legado.app.ui.config + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.text.InputType +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.MenuProvider +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppWebDav +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.storage.Backup +import io.legado.app.help.storage.BackupConfig +import io.legado.app.help.storage.ImportOldData +import io.legado.app.help.storage.Restore +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.FileDoc +import io.legado.app.utils.applyTint +import io.legado.app.utils.checkWrite +import io.legado.app.utils.getPrefString +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.launch +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.toEditable +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.init.appCtx + +class BackupConfigFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener, + MenuProvider { + + private val viewModel by activityViewModels() + private val waitDialog by lazy { WaitDialog(requireContext()) } + private var backupJob: Job? = null + private var restoreJob: Job? = null + + private val selectBackupPath = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + if (uri.isContentScheme()) { + AppConfig.backupPath = uri.toString() + } else { + AppConfig.backupPath = uri.path + } + } + } + private val backupDir = registerForActivityResult(HandleFileContract()) { result -> + result.uri?.let { uri -> + if (uri.isContentScheme()) { + AppConfig.backupPath = uri.toString() + backup(uri.toString()) + } else { + uri.path?.let { path -> + AppConfig.backupPath = path + backup(path) + } + } + } + } + private val restoreDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + waitDialog.setText("恢复中…") + waitDialog.show() + val task = Coroutine.async { + Restore.restore(appCtx, uri) + }.onFinally { + waitDialog.dismiss() + } + waitDialog.setOnCancelListener { + task.cancel() + } + } + } + private val restoreOld = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + ImportOldData.importUri(appCtx, uri) + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_backup) + findPreference(PreferKey.webDavPassword)?.let { + it.setOnBindEditTextListener { editText -> + editText.inputType = + InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT + editText.setSelection(editText.text.length) + } + } + findPreference(PreferKey.webDavDir)?.let { + it.setOnBindEditTextListener { editText -> + editText.text = AppConfig.webDavDir?.toEditable() + editText.setSelection(editText.text.length) + } + } + findPreference(PreferKey.webDavDeviceName)?.let { + it.setOnBindEditTextListener { editText -> + editText.text = AppConfig.webDavDeviceName?.toEditable() + editText.setSelection(editText.text.length) + } + } + upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl)) + upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount)) + upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword)) + upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir) + upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName) + upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath)) + findPreference("web_dav_restore") + ?.onLongClick { + restoreFromLocal() + true + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.setTitle(R.string.backup_restore) + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + listView.setEdgeEffectColor(primaryColor) + activity?.addMenuProvider(this, viewLifecycleOwner) + if (!LocalConfig.backupHelpVersionIsLast) { + showHelp("webDavHelp") + } + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.backup_restore, menu) + menu.applyTint(requireContext()) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.menu_help -> { + showHelp("webDavHelp") + return true + } + + R.id.menu_log -> showDialogFragment() + } + return false + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.backupPath -> upPreferenceSummary(key, getPrefString(key)) + PreferKey.webDavUrl, + PreferKey.webDavAccount, + PreferKey.webDavPassword, + PreferKey.webDavDir -> listView.post { + upPreferenceSummary(key, appCtx.getPrefString(key)) + viewModel.upWebDavConfig() + } + + PreferKey.webDavDeviceName -> upPreferenceSummary(key, getPrefString(key)) + } + } + + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.webDavUrl -> + if (value.isNullOrBlank()) { + preference.summary = getString(R.string.web_dav_url_s) + } else { + preference.summary = value + } + + PreferKey.webDavAccount -> + if (value.isNullOrBlank()) { + preference.summary = getString(R.string.web_dav_account_s) + } else { + preference.summary = value + } + + PreferKey.webDavPassword -> + if (value.isNullOrEmpty()) { + preference.summary = getString(R.string.web_dav_pw_s) + } else { + preference.summary = "*".repeat(value.length) + } + + PreferKey.webDavDir -> preference.summary = when (value) { + null -> "legado" + else -> value + } + + else -> { + if (preference is ListPreference) { + val index = preference.findIndexOfValue(value) + // Set the summary to reflect the new value. + preference.summary = if (index >= 0) preference.entries[index] else null + } else { + preference.summary = value + } + } + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + PreferKey.backupPath -> selectBackupPath.launch() + PreferKey.restoreIgnore -> backupIgnore() + "web_dav_backup" -> backup() + "web_dav_restore" -> restore() + "import_old" -> restoreOld.launch() + } + return super.onPreferenceTreeClick(preference) + } + + /** + * 备份忽略设置 + */ + private fun backupIgnore() { + val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) { + BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false + } + alert(R.string.restore_ignore) { + multiChoiceItems(BackupConfig.ignoreTitle, checkedItems) { _, which, isChecked -> + BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[which]] = isChecked + } + onDismiss { + BackupConfig.saveIgnoreConfig() + } + } + } + + + fun backup() { + val backupPath = AppConfig.backupPath + if (backupPath.isNullOrEmpty()) { + backupDir.launch() + } else { + if (backupPath.isContentScheme()) { + lifecycleScope.launch { + val canWrite = withContext(IO) { + FileDoc.fromDir(backupPath).checkWrite() + } + if (canWrite) { + backup(backupPath) + } else { + backupDir.launch() + } + } + } else { + backupUsePermission(backupPath) + } + } + } + + private fun backup(backupPath: String) { + waitDialog.setText("备份中…") + waitDialog.setOnCancelListener { + backupJob?.cancel() + } + waitDialog.show() + backupJob?.cancel() + backupJob = lifecycleScope.launch { + try { + Backup.backupLocked(requireContext(), backupPath) + appCtx.toastOnUi(R.string.backup_success) + } catch (e: Throwable) { + ensureActive() + AppLog.put("备份出错\n${e.localizedMessage}", e) + appCtx.toastOnUi( + appCtx.getString( + R.string.backup_fail, + e.localizedMessage + ) + ) + } finally { + ensureActive() + waitDialog.dismiss() + } + } + } + + private fun backupUsePermission(path: String) { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + backup(path) + } + .request() + } + + fun restore() { + waitDialog.setText(R.string.loading) + waitDialog.setOnCancelListener { + restoreJob?.cancel() + } + waitDialog.show() + Coroutine.async { + restoreJob = coroutineContext[Job] + showRestoreDialog(requireContext()) + }.onError { + AppLog.put("恢复备份出错WebDavError\n${it.localizedMessage}", it) + if (context == null) { + return@onError + } + alert { + setTitle(R.string.restore) + setMessage("WebDavError\n${it.localizedMessage}\n将从本地备份恢复。") + okButton { + restoreFromLocal() + } + cancelButton() + } + }.onFinally { + waitDialog.dismiss() + } + } + + private suspend fun showRestoreDialog(context: Context) { + val names = withContext(IO) { AppWebDav.getBackupNames() } + if (AppWebDav.isJianGuoYun && names.size > 700) { + context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份") + } + if (names.isNotEmpty()) { + currentCoroutineContext().ensureActive() + withContext(Main) { + context.selector( + title = context.getString(R.string.select_restore_file), + items = names + ) { _, index -> + if (index in 0 until names.size) { + listView.post { + restoreWebDav(names[index]) + } + } + } + } + } else { + throw NoStackTraceException("Web dav no back up file") + } + } + + private fun restoreWebDav(name: String) { + waitDialog.setText("恢复中…") + waitDialog.show() + val task = Coroutine.async { + AppWebDav.restoreWebDav(name) + }.onError { + AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it) + appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}") + }.onFinally { + waitDialog.dismiss() + } + waitDialog.setOnCancelListener { + task.cancel() + } + } + + private fun restoreFromLocal() { + restoreDoc.launch { + title = getString(R.string.select_restore_file) + mode = HandleFileContract.FILE + allowExtensions = arrayOf("zip") + } + } + + override fun onDestroyView() { + super.onDestroyView() + waitDialog.dismiss() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/CheckSourceConfig.kt b/app/src/main/java/io/legado/app/ui/config/CheckSourceConfig.kt new file mode 100755 index 000000000..2636870c6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/CheckSourceConfig.kt @@ -0,0 +1,104 @@ +package io.legado.app.ui.config + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogCheckSourceConfigBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.CheckSource +import io.legado.app.utils.putPrefString +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.views.onClick + +class CheckSourceConfig : BaseDialogFragment(R.layout.dialog_check_source_config) { + + private val binding by viewBinding(DialogCheckSourceConfigBinding::bind) + + //允许的最小超时时间,秒 + private val minTimeout = 0L + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.run { + checkSearch.onClick { + if (!checkSearch.isChecked && !checkDiscovery.isChecked) { + checkDiscovery.isChecked = true + } + } + checkDiscovery.onClick { + if (!checkSearch.isChecked && !checkDiscovery.isChecked) { + checkSearch.isChecked = true + } + } + checkInfo.onClick { + if (!checkInfo.isChecked) { + checkCategory.isChecked = false + checkContent.isChecked = false + checkCategory.isEnabled = false + checkContent.isEnabled = false + } else { + checkCategory.isEnabled = true + } + } + checkCategory.onClick { + if (!checkCategory.isChecked) { + checkContent.isChecked = false + checkContent.isEnabled = false + } else { + checkContent.isEnabled = true + } + } + } + CheckSource.run { + binding.checkSourceTimeout.setText((timeout / 1000).toString()) + binding.checkSearch.isChecked = checkSearch + binding.checkDiscovery.isChecked = checkDiscovery + binding.checkInfo.isChecked = checkInfo + binding.checkCategory.isChecked = checkCategory + binding.checkContent.isChecked = checkContent + binding.checkCategory.isEnabled = checkInfo + binding.checkContent.isEnabled = checkCategory + binding.tvCancel.onClick { + dismiss() + } + binding.tvOk.onClick { + val text = binding.checkSourceTimeout.text.toString() + when { + text.isBlank() -> { + toastOnUi("${getString(R.string.timeout)}${getString(R.string.cannot_empty)}") + return@onClick + } + text.toLong() <= minTimeout -> { + toastOnUi( + "${getString(R.string.timeout)}${getString(R.string.less_than)}${minTimeout}${ + getString( + R.string.seconds + ) + }" + ) + return@onClick + } + else -> timeout = text.toLong() * 1000 + } + checkSearch = binding.checkSearch.isChecked + checkDiscovery = binding.checkDiscovery.isChecked + checkInfo = binding.checkInfo.isChecked + checkCategory = binding.checkCategory.isChecked + checkContent = binding.checkContent.isChecked + putConfig() + putPrefString(PreferKey.checkSource, summary) + dismiss() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt b/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt new file mode 100755 index 000000000..54d4894d4 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.config + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.fragment.app.Fragment +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.EventBus +import io.legado.app.databinding.ActivityConfigBinding +import io.legado.app.utils.observeEvent +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class ConfigActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivityConfigBinding::inflate) + override val viewModel by viewModels() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + when (val configTag = intent.getStringExtra("configTag")) { + ConfigTag.OTHER_CONFIG -> replaceFragment(configTag) + ConfigTag.THEME_CONFIG -> replaceFragment(configTag) + ConfigTag.BACKUP_CONFIG -> replaceFragment(configTag) + ConfigTag.COVER_CONFIG -> replaceFragment(configTag) + ConfigTag.WELCOME_CONFIG -> replaceFragment(configTag) + else -> finish() + } + } + + override fun setTitle(resId: Int) { + super.setTitle(resId) + binding.titleBar.setTitle(resId) + } + + inline fun replaceFragment(configTag: String) { + intent.putExtra("configTag", configTag) + @Suppress("DEPRECATION") + val configFragment = supportFragmentManager.findFragmentByTag(configTag) + ?: T::class.java.newInstance() + supportFragmentManager.beginTransaction() + .replace(R.id.configFrameLayout, configFragment, configTag) + .commit() + } + + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.RECREATE) { + recreate() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt b/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt new file mode 100755 index 000000000..81148bd58 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/ConfigTag.kt @@ -0,0 +1,11 @@ +package io.legado.app.ui.config + +object ConfigTag { + + const val OTHER_CONFIG = "otherConfig" + const val THEME_CONFIG = "themeConfig" + const val BACKUP_CONFIG = "backupConfig" + const val COVER_CONFIG = "coverConfig" + const val WELCOME_CONFIG = "welcomeConfig" + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt new file mode 100755 index 000000000..fa6372c2d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/ConfigViewModel.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.config + +import android.app.Application +import android.content.Context +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.utils.FileUtils +import io.legado.app.utils.restart +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.delay +import splitties.init.appCtx + +class ConfigViewModel(application: Application) : BaseViewModel(application) { + + fun upWebDavConfig() { + execute { + AppWebDav.upConfig() + } + } + + fun clearCache() { + execute { + BookHelp.clearCache() + FileUtils.delete(context.cacheDir.absolutePath) + }.onSuccess { + context.toastOnUi(R.string.clear_cache_success) + } + } + + fun clearWebViewData() { + execute { + FileUtils.delete(context.getDir("webview", Context.MODE_PRIVATE)) + FileUtils.delete(context.getDir("hws_webview", Context.MODE_PRIVATE), true) + context.toastOnUi(R.string.clear_webview_data_success) + delay(3000) + appCtx.restart() + } + } + + fun shrinkDatabase() { + execute { + appDb.openHelper.writableDatabase.execSQL("VACUUM") + }.onSuccess { + context.toastOnUi(R.string.success) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/config/CoverConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/CoverConfigFragment.kt new file mode 100755 index 000000000..0a63d3346 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/CoverConfigFragment.kt @@ -0,0 +1,190 @@ +package io.legado.app.ui.config + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.prefs.SwitchPreference +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.BookCover +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefString +import io.legado.app.utils.inputStream +import io.legado.app.utils.putPrefString +import io.legado.app.utils.readUri +import io.legado.app.utils.removePref +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx +import java.io.FileOutputStream + +class CoverConfigFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val requestCodeCover = 111 + private val requestCodeCoverDark = 112 + private val selectImage = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + when (it.requestCode) { + requestCodeCover -> setCoverFromUri(PreferKey.defaultCover, uri) + requestCodeCoverDark -> setCoverFromUri(PreferKey.defaultCoverDark, uri) + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_cover) + upPreferenceSummary(PreferKey.defaultCover, getPrefString(PreferKey.defaultCover)) + upPreferenceSummary(PreferKey.defaultCoverDark, getPrefString(PreferKey.defaultCoverDark)) + findPreference(PreferKey.coverShowAuthor) + ?.isEnabled = getPrefBoolean(PreferKey.coverShowName) + findPreference(PreferKey.coverShowAuthorN) + ?.isEnabled = getPrefBoolean(PreferKey.coverShowNameN) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.setTitle(R.string.cover_config) + listView.setEdgeEffectColor(primaryColor) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + sharedPreferences ?: return + when (key) { + PreferKey.defaultCover, + PreferKey.defaultCoverDark -> { + upPreferenceSummary(key, getPrefString(key)) + } + + PreferKey.coverShowName -> { + findPreference(PreferKey.coverShowAuthor) + ?.isEnabled = getPrefBoolean(key) + BookCover.upDefaultCover() + } + + PreferKey.coverShowNameN -> { + findPreference(PreferKey.coverShowAuthorN) + ?.isEnabled = getPrefBoolean(key) + BookCover.upDefaultCover() + } + + PreferKey.coverShowAuthor, + PreferKey.coverShowAuthorN -> { + BookCover.upDefaultCover() + } + } + } + + @SuppressLint("PrivateResource") + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + "coverRule" -> showDialogFragment(CoverRuleConfigDialog()) + PreferKey.defaultCover -> + if (getPrefString(preference.key).isNullOrEmpty()) { + selectImage.launch { + requestCode = requestCodeCover + mode = HandleFileContract.IMAGE + } + } else { + context?.selector( + items = arrayListOf( + getString(R.string.delete), + getString(R.string.select_image) + ) + ) { _, i -> + if (i == 0) { + removePref(preference.key) + BookCover.upDefaultCover() + } else { + selectImage.launch { + requestCode = requestCodeCover + mode = HandleFileContract.IMAGE + } + } + } + } + + PreferKey.defaultCoverDark -> + if (getPrefString(preference.key).isNullOrEmpty()) { + selectImage.launch { + requestCode = requestCodeCoverDark + mode = HandleFileContract.IMAGE + } + } else { + context?.selector( + items = arrayListOf( + getString(R.string.delete), + getString(R.string.select_image) + ) + ) { _, i -> + if (i == 0) { + removePref(preference.key) + BookCover.upDefaultCover() + } else { + selectImage.launch { + requestCode = requestCodeCoverDark + mode = HandleFileContract.IMAGE + } + } + } + } + } + return super.onPreferenceTreeClick(preference) + } + + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.defaultCover, + PreferKey.defaultCoverDark -> preference.summary = if (value.isNullOrBlank()) { + getString(R.string.select_image) + } else { + value + } + + else -> preference.summary = value + } + } + + private fun setCoverFromUri(preferenceKey: String, uri: Uri) { + readUri(uri) { fileDoc, inputStream -> + kotlin.runCatching { + var file = requireContext().externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val fileName = uri.inputStream(requireContext()).getOrThrow().use { + MD5Utils.md5Encode(it) + ".$suffix" + } + file = FileUtils.createFileIfNotExist(file, "covers", fileName) + FileOutputStream(file).use { + inputStream.copyTo(it) + } + putPrefString(preferenceKey, file.absolutePath) + BookCover.upDefaultCover() + }.onFailure { + appCtx.toastOnUi(it.localizedMessage) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/CoverRuleConfigDialog.kt b/app/src/main/java/io/legado/app/ui/config/CoverRuleConfigDialog.kt new file mode 100755 index 000000000..8d8ae8437 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/CoverRuleConfigDialog.kt @@ -0,0 +1,68 @@ +package io.legado.app.ui.config + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogCoverRuleConfigBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.BookCover +import io.legado.app.utils.GSON +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.views.onClick + +class CoverRuleConfigDialog : BaseDialogFragment(R.layout.dialog_cover_rule_config) { + + val binding by viewBinding(DialogCoverRuleConfigBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + initData() + binding.tvCancel.onClick { + dismissAllowingStateLoss() + } + binding.tvOk.onClick { + val enable = binding.cbEnable.isChecked + val searchUrl = binding.editSearchUrl.text?.toString() + val coverRule = binding.editCoverUrlRule.text?.toString() + if (searchUrl.isNullOrBlank() || coverRule.isNullOrBlank()) { + toastOnUi("搜索url和cover规则不能为空") + } else { + BookCover.CoverRule(enable, searchUrl, coverRule).let { config -> + BookCover.saveCoverRule(config) + } + dismissAllowingStateLoss() + } + } + binding.tvFooterLeft.onClick { + BookCover.delCoverRule() + dismissAllowingStateLoss() + } + } + + private fun initData() { + lifecycleScope.launch { + val rule = withContext(IO) { + BookCover.getCoverRule() + } + Log.e("coverRule", GSON.toJson(rule)) + binding.cbEnable.isChecked = rule.enable + binding.editSearchUrl.setText(rule.searchUrl) + binding.editCoverUrlRule.setText(rule.coverRule) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/DirectLinkUploadConfig.kt b/app/src/main/java/io/legado/app/ui/config/DirectLinkUploadConfig.kt new file mode 100755 index 000000000..82770ef7a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/DirectLinkUploadConfig.kt @@ -0,0 +1,130 @@ +package io.legado.app.ui.config + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogDirectLinkUploadConfigBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.GSON +import io.legado.app.utils.applyTint +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getClipText +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.init.appCtx +import splitties.views.onClick + +class DirectLinkUploadConfig : BaseDialogFragment(R.layout.dialog_direct_link_upload_config), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogDirectLinkUploadConfigBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.inflateMenu(R.menu.direct_link_upload_config) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + upView(DirectLinkUpload.getRule()) + binding.tvCancel.onClick { + dismiss() + } + binding.tvFooterLeft.onClick { + test() + } + binding.tvOk.onClick { + getRule()?.let { rule -> + DirectLinkUpload.putConfig(rule) + dismiss() + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_import_default -> importDefault() + R.id.menu_copy_rule -> getRule()?.let { rule -> + requireContext().sendToClip(GSON.toJson(rule)) + } + + R.id.menu_paste_rule -> runCatching { + requireContext().getClipText()!!.let { + val rule = GSON.fromJsonObject(it).getOrThrow() + upView(rule) + } + }.onFailure { + toastOnUi("剪贴板为空或格式不对") + } + } + return true + } + + private fun upView(rule: DirectLinkUpload.Rule) { + binding.editUploadUrl.setText(rule.uploadUrl) + binding.editDownloadUrlRule.setText(rule.downloadUrlRule) + binding.editSummary.setText(rule.summary) + binding.cbCompress.isChecked = rule.compress + } + + private fun getRule(): DirectLinkUpload.Rule? { + val uploadUrl = binding.editUploadUrl.text?.toString() + val downloadUrlRule = binding.editDownloadUrlRule.text?.toString() + val summary = binding.editSummary.text?.toString() + val compress = binding.cbCompress.isChecked + if (uploadUrl.isNullOrBlank()) { + toastOnUi("上传Url不能为空") + return null + } + if (downloadUrlRule.isNullOrBlank()) { + toastOnUi("下载Url规则不能为空") + return null + } + if (summary.isNullOrBlank()) { + toastOnUi("注释不能为空") + return null + } + return DirectLinkUpload.Rule(uploadUrl, downloadUrlRule, summary, compress) + } + + private fun importDefault() { + requireContext().selector(DirectLinkUpload.defaultRules) { _, rule, _ -> + upView(rule) + } + } + + private fun test() { + val rule = getRule() ?: return + execute { + DirectLinkUpload.upLoad("test.json", "{}", "application/json", rule) + }.onError { + alertTestResult(it.localizedMessage ?: "ERROR") + }.onSuccess { result -> + alertTestResult(result) + } + } + + private fun alertTestResult(result: String) { + alert { + setTitle("result") + setMessage(result) + okButton() + negativeButton(R.string.copy_text) { + appCtx.sendToClip(result) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt new file mode 100755 index 000000000..df2c93a30 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -0,0 +1,336 @@ +package io.legado.app.ui.config + +import android.annotation.SuppressLint +import android.content.ComponentName +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import androidx.core.view.postDelayed +import androidx.fragment.app.activityViewModels +import androidx.preference.ListPreference +import androidx.preference.Preference +import com.jeremyliao.liveeventbus.LiveEventBus +import io.legado.app.R +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.AppFreezeMonitor +import io.legado.app.help.DispatchersMonitor +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.CheckSource +import io.legado.app.model.ImageProvider +import io.legado.app.receiver.SharedReceiverActivity +import io.legado.app.service.WebService +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.number.NumberPickerDialog +import io.legado.app.utils.LogUtils +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.postEvent +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.putPrefString +import io.legado.app.utils.removePref +import io.legado.app.utils.restart +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import splitties.init.appCtx + +/** + * 其它设置 + */ +class OtherConfigFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val viewModel by activityViewModels() + private val packageManager = appCtx.packageManager + private val componentName = ComponentName( + appCtx, + SharedReceiverActivity::class.java.name + ) + private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { + it.uri?.let { treeUri -> + AppConfig.defaultBookTreeUri = treeUri.toString() + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + putPrefBoolean(PreferKey.processText, isProcessTextEnabled()) + addPreferencesFromResource(R.xml.pref_config_other) + upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent) + upPreferenceSummary(PreferKey.preDownloadNum, AppConfig.preDownloadNum.toString()) + upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString()) + upPreferenceSummary(PreferKey.webPort, AppConfig.webPort.toString()) + AppConfig.defaultBookTreeUri?.let { + upPreferenceSummary(PreferKey.defaultBookTreeUri, it) + } + upPreferenceSummary(PreferKey.checkSource, CheckSource.summary) + upPreferenceSummary(PreferKey.bitmapCacheSize, AppConfig.bitmapCacheSize.toString()) + upPreferenceSummary(PreferKey.imageRetainNum, AppConfig.imageRetainNum.toString()) + upPreferenceSummary(PreferKey.sourceEditMaxLine, AppConfig.sourceEditMaxLine.toString()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.setTitle(R.string.other_setting) + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + listView.setEdgeEffectColor(primaryColor) + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + PreferKey.userAgent -> showUserAgentDialog() + PreferKey.defaultBookTreeUri -> localBookTreeSelect.launch { + title = getString(R.string.select_book_folder) + mode = HandleFileContract.DIR_SYS + } + + PreferKey.preDownloadNum -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.pre_download)) + .setMaxValue(9999) + .setMinValue(0) + .setValue(AppConfig.preDownloadNum) + .show { + AppConfig.preDownloadNum = it + } + + PreferKey.threadCount -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.threads_num_title)) + .setMaxValue(999) + .setMinValue(1) + .setValue(AppConfig.threadCount) + .show { + AppConfig.threadCount = it + } + + PreferKey.webPort -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.web_port_title)) + .setMaxValue(60000) + .setMinValue(1024) + .setValue(AppConfig.webPort) + .show { + AppConfig.webPort = it + } + + PreferKey.cleanCache -> clearCache() + PreferKey.uploadRule -> showDialogFragment() + PreferKey.checkSource -> showDialogFragment() + PreferKey.bitmapCacheSize -> { + NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.bitmap_cache_size)) + .setMaxValue(1024) + .setMinValue(1) + .setValue(AppConfig.bitmapCacheSize) + .show { + AppConfig.bitmapCacheSize = it + ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize) + } + } + PreferKey.imageRetainNum -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.image_retain_number)) + .setMaxValue(999) + .setMinValue(0) + .setValue(AppConfig.imageRetainNum) + .show { + AppConfig.imageRetainNum = it + } + + PreferKey.sourceEditMaxLine -> { + NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.source_edit_text_max_line)) + .setMaxValue(Int.MAX_VALUE) + .setMinValue(10) + .setValue(AppConfig.sourceEditMaxLine) + .show { + AppConfig.sourceEditMaxLine = it + } + } + + PreferKey.clearWebViewData -> clearWebViewData() + "localPassword" -> alertLocalPassword() + PreferKey.shrinkDatabase -> shrinkDatabase() + } + return super.onPreferenceTreeClick(preference) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.preDownloadNum -> { + upPreferenceSummary(key, AppConfig.preDownloadNum.toString()) + } + + PreferKey.threadCount -> { + upPreferenceSummary(key, AppConfig.threadCount.toString()) + postEvent(PreferKey.threadCount, "") + } + + PreferKey.webPort -> { + upPreferenceSummary(key, AppConfig.webPort.toString()) + if (WebService.isRun) { + WebService.stop(requireContext()) + WebService.start(requireContext()) + } + } + + PreferKey.defaultBookTreeUri -> { + upPreferenceSummary(key, AppConfig.defaultBookTreeUri) + } + + PreferKey.recordLog -> { + AppConfig.recordLog = appCtx.getPrefBoolean(PreferKey.recordLog) + LogUtils.upLevel() + LogUtils.logDeviceInfo() + LiveEventBus.config().enableLogger(AppConfig.recordLog) + AppFreezeMonitor.init(appCtx) + DispatchersMonitor.init() + } + + PreferKey.processText -> sharedPreferences?.let { + setProcessTextEnable(it.getBoolean(key, true)) + } + + PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true) + PreferKey.language -> listView.postDelayed(1000) { + appCtx.restart() + } + + PreferKey.userAgent -> listView.post { + upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent) + } + + PreferKey.checkSource -> listView.post { + upPreferenceSummary(PreferKey.checkSource, CheckSource.summary) + } + + PreferKey.bitmapCacheSize -> { + upPreferenceSummary(key, AppConfig.bitmapCacheSize.toString()) + } + + PreferKey.imageRetainNum -> { + upPreferenceSummary(key, AppConfig.imageRetainNum.toString()) + } + + PreferKey.sourceEditMaxLine -> { + upPreferenceSummary(key, AppConfig.sourceEditMaxLine.toString()) + } + } + } + + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.preDownloadNum -> preference.summary = + getString(R.string.pre_download_s, value) + + PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value) + PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value) + PreferKey.bitmapCacheSize -> preference.summary = + getString(R.string.bitmap_cache_size_summary, value) + PreferKey.imageRetainNum -> preference.summary = + getString(R.string.image_retain_number_summary, value) + + PreferKey.sourceEditMaxLine -> preference.summary = + getString(R.string.source_edit_max_line_summary, value) + + else -> if (preference is ListPreference) { + val index = preference.findIndexOfValue(value) + // Set the summary to reflect the new value. + preference.summary = if (index >= 0) preference.entries[index] else null + } else { + preference.summary = value + } + } + } + + @SuppressLint("InflateParams") + private fun showUserAgentDialog() { + alert(getString(R.string.user_agent)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.user_agent) + editView.setText(AppConfig.userAgent) + } + customView { alertBinding.root } + okButton { + val userAgent = alertBinding.editView.text?.toString() + if (userAgent.isNullOrBlank()) { + removePref(PreferKey.userAgent) + } else { + putPrefString(PreferKey.userAgent, userAgent) + } + } + cancelButton() + } + } + + private fun clearCache() { + requireContext().alert( + titleResource = R.string.clear_cache, + messageResource = R.string.sure_del + ) { + okButton { + viewModel.clearCache() + } + noButton() + } + } + + private fun shrinkDatabase() { + alert(R.string.sure, R.string.shrink_database) { + okButton { + viewModel.shrinkDatabase() + } + noButton() + } + } + + private fun clearWebViewData() { + alert(R.string.clear_webview_data, R.string.sure_del) { + okButton { + viewModel.clearWebViewData() + } + noButton() + } + } + + private fun isProcessTextEnabled(): Boolean { + return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + + private fun setProcessTextEnable(enable: Boolean) { + if (enable) { + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP + ) + } else { + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP + ) + } + } + + private fun alertLocalPassword() { + context?.alert(R.string.set_local_password, R.string.set_local_password_summary) { + val editTextBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "password" + } + customView { + editTextBinding.root + } + okButton { + LocalConfig.password = editTextBinding.editView.text.toString() + } + cancelButton() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt new file mode 100755 index 000000000..f9dd5e065 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt @@ -0,0 +1,359 @@ +package io.legado.app.ui.config + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.SeekBar +import androidx.core.view.MenuProvider +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.base.AppContextWrapper +import io.legado.app.constant.AppConst +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogImageBlurringBinding +import io.legado.app.help.LauncherIconHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.prefs.ColorPreference +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.number.NumberPickerDialog +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.applyTint +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.getPrefString +import io.legado.app.utils.inputStream +import io.legado.app.utils.postEvent +import io.legado.app.utils.putPrefInt +import io.legado.app.utils.putPrefString +import io.legado.app.utils.readUri +import io.legado.app.utils.removePref +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx +import java.io.FileOutputStream + + +@Suppress("SameParameterValue") +class ThemeConfigFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener, + MenuProvider { + + private val requestCodeBgLight = 121 + private val requestCodeBgDark = 122 + private val selectImage = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + when (it.requestCode) { + requestCodeBgLight -> setBgFromUri(uri, PreferKey.bgImage) { + upTheme(false) + } + + requestCodeBgDark -> setBgFromUri(uri, PreferKey.bgImageN) { + upTheme(true) + } + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_theme) + if (Build.VERSION.SDK_INT < 26) { + preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon) + } + upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage)) + upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN)) + upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString()) + upPreferenceSummary(PreferKey.fontScale) + findPreference(PreferKey.cBackground)?.let { + it.onSaveColor = { color -> + if (!ColorUtils.isColorLight(color)) { + toastOnUi(R.string.day_background_too_dark) + true + } else { + false + } + } + } + findPreference(PreferKey.cNBackground)?.let { + it.onSaveColor = { color -> + if (ColorUtils.isColorLight(color)) { + toastOnUi(R.string.night_background_too_light) + true + } else { + false + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.setTitle(R.string.theme_setting) + listView.setEdgeEffectColor(primaryColor) + activity?.addMenuProvider(this, viewLifecycleOwner) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.theme_config, menu) + menu.applyTint(requireContext()) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.menu_theme_mode -> { + AppConfig.isNightTheme = !AppConfig.isNightTheme + ThemeConfig.applyDayNight(requireContext()) + return true + } + } + return false + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + sharedPreferences ?: return + when (key) { + PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key)) + PreferKey.transparentStatusBar -> recreateActivities() + PreferKey.immNavigationBar -> recreateActivities() + PreferKey.cPrimary, + PreferKey.cAccent, + PreferKey.cBackground, + PreferKey.cBBackground -> { + upTheme(false) + } + + PreferKey.cNPrimary, + PreferKey.cNAccent, + PreferKey.cNBackground, + PreferKey.cNBBackground -> { + upTheme(true) + } + + PreferKey.bgImage, + PreferKey.bgImageN -> { + upPreferenceSummary(key, getPrefString(key)) + } + } + + } + + @SuppressLint("PrivateResource") + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (val key = preference.key) { + PreferKey.barElevation -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.bar_elevation)) + .setMaxValue(32) + .setMinValue(0) + .setValue(AppConfig.elevation) + .setCustomButton((R.string.btn_default_s)) { + AppConfig.elevation = AppConst.sysElevation + recreateActivities() + } + .show { + AppConfig.elevation = it + recreateActivities() + } + + PreferKey.fontScale -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.font_scale)) + .setMaxValue(16) + .setMinValue(8) + .setValue(10) + .setCustomButton((R.string.btn_default_s)) { + putPrefInt(PreferKey.fontScale, 0) + recreateActivities() + } + .show { + putPrefInt(PreferKey.fontScale, it) + recreateActivities() + } + + PreferKey.bgImage -> selectBgAction(false) + PreferKey.bgImageN -> selectBgAction(true) + "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") + "saveDayTheme", + "saveNightTheme" -> alertSaveTheme(key) + + "coverConfig" -> startActivity { + putExtra("configTag", ConfigTag.COVER_CONFIG) + } + + "welcomeStyle" -> startActivity { + putExtra("configTag", ConfigTag.WELCOME_CONFIG) + } + } + return super.onPreferenceTreeClick(preference) + } + + @SuppressLint("InflateParams") + private fun alertSaveTheme(key: String) { + alert(R.string.theme_name) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "name" + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { themeName -> + when (key) { + "saveDayTheme" -> { + ThemeConfig.saveDayTheme(requireContext(), themeName) + } + + "saveNightTheme" -> { + ThemeConfig.saveNightTheme(requireContext(), themeName) + } + } + } + } + cancelButton() + } + } + + private fun selectBgAction(isNight: Boolean) { + val bgKey = if (isNight) PreferKey.bgImageN else PreferKey.bgImage + val blurringKey = if (isNight) PreferKey.bgImageNBlurring else PreferKey.bgImageBlurring + val actions = arrayListOf( + getString(R.string.background_image_blurring), + getString(R.string.select_image) + ) + if (!getPrefString(bgKey).isNullOrEmpty()) { + actions.add(getString(R.string.delete)) + } + context?.selector(items = actions) { _, i -> + when (i) { + 0 -> alertImageBlurring(blurringKey) { + upTheme(isNight) + } + + 1 -> { + if (isNight) { + selectImage.launch { + requestCode = requestCodeBgDark + mode = HandleFileContract.IMAGE + } + } else { + selectImage.launch { + requestCode = requestCodeBgLight + mode = HandleFileContract.IMAGE + } + } + } + + 2 -> { + removePref(bgKey) + upTheme(isNight) + } + } + } + } + + private fun alertImageBlurring(preferKey: String, success: () -> Unit) { + alert(R.string.background_image_blurring) { + val alertBinding = DialogImageBlurringBinding.inflate(layoutInflater).apply { + getPrefInt(preferKey, 0).let { + seekBar.progress = it + textViewValue.text = it.toString() + } + seekBar.setOnSeekBarChangeListener(object : SeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar, + progress: Int, + fromUser: Boolean + ) { + textViewValue.text = progress.toString() + } + }) + } + customView { alertBinding.root } + okButton { + alertBinding.seekBar.progress.let { + putPrefInt(preferKey, it) + success.invoke() + } + } + cancelButton() + } + } + + private fun upTheme(isNightTheme: Boolean) { + if (AppConfig.isNightTheme == isNightTheme) { + listView.post { + ThemeConfig.applyTheme(requireContext()) + recreateActivities() + } + } + } + + private fun recreateActivities() { + postEvent(EventBus.RECREATE, "") + } + + private fun upPreferenceSummary(preferenceKey: String, value: String? = null) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.barElevation -> preference.summary = + getString(R.string.bar_elevation_s, value) + + PreferKey.fontScale -> { + val fontScale = AppContextWrapper.getFontScale(requireContext()) + preference.summary = getString(R.string.font_scale_summary, fontScale) + } + + PreferKey.bgImage, + PreferKey.bgImageN -> preference.summary = if (value.isNullOrBlank()) { + getString(R.string.select_image) + } else { + value + } + + else -> preference.summary = value + } + } + + private fun setBgFromUri(uri: Uri, preferenceKey: String, success: () -> Unit) { + readUri(uri) { fileDoc, inputStream -> + kotlin.runCatching { + var file = requireContext().externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val fileName = uri.inputStream(requireContext()).getOrThrow().use { + MD5Utils.md5Encode(it) + ".$suffix" + } + file = FileUtils.createFileIfNotExist(file, preferenceKey, fileName) + FileOutputStream(file).use { + inputStream.copyTo(it) + } + putPrefString(preferenceKey, file.absolutePath) + success() + }.onFailure { + appCtx.toastOnUi(it.localizedMessage) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt b/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt new file mode 100755 index 000000000..c6817338f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/ThemeListDialog.kt @@ -0,0 +1,121 @@ +package io.legado.app.ui.config + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemThemeConfigBinding +import io.legado.app.help.config.ThemeConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class ThemeListDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { Adapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.theme_list) + initView() + initMenu() + initData() + } + + private fun initView() = binding.run { + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + } + + private fun initMenu() = binding.run { + toolBar.setOnMenuItemClickListener(this@ThemeListDialog) + toolBar.inflateMenu(R.menu.theme_list) + toolBar.menu.applyTint(requireContext()) + } + + fun initData() { + adapter.setItems(ThemeConfig.configList) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_import -> { + requireContext().getClipText()?.let { + if (ThemeConfig.addConfig(it)) { + initData() + } else { + toastOnUi("格式不对,添加失败") + } + } + } + } + return true + } + + fun delete(index: Int) { + alert(R.string.delete, R.string.sure_del) { + yesButton { + ThemeConfig.delConfig(index) + initData() + } + noButton() + } + } + + fun share(index: Int) { + val json = GSON.toJson(ThemeConfig.configList[index]) + requireContext().share(json, "主题分享") + } + + inner class Adapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemThemeConfigBinding { + return ItemThemeConfigBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemThemeConfigBinding, + item: ThemeConfig.Config, + payloads: MutableList + ) { + binding.apply { + tvName.text = item.themeName + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemThemeConfigBinding) { + binding.apply { + root.setOnClickListener { + ThemeConfig.applyConfig(context, ThemeConfig.configList[holder.layoutPosition]) + } + ivShare.setOnClickListener { + share(holder.layoutPosition) + } + ivDelete.setOnClickListener { + delete(holder.layoutPosition) + } + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/WelcomeConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/WelcomeConfigFragment.kt new file mode 100755 index 000000000..15e7bb4e0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/WelcomeConfigFragment.kt @@ -0,0 +1,213 @@ +package io.legado.app.ui.config + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.prefs.SwitchPreference +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.primaryColor +import io.legado.app.model.BookCover +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getPrefString +import io.legado.app.utils.inputStream +import io.legado.app.utils.putPrefString +import io.legado.app.utils.readUri +import io.legado.app.utils.removePref +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx +import java.io.FileOutputStream + +class WelcomeConfigFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener { + + private val requestWelcomeImage = 221 + private val requestWelcomeImageDark = 222 + private val selectImage = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + when (it.requestCode) { + requestWelcomeImage -> setCoverFromUri(PreferKey.welcomeImage, uri) + requestWelcomeImageDark -> setCoverFromUri(PreferKey.welcomeImageDark, uri) + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_welcome) + val welcomeImage = AppConfig.welcomeImage + val welcomeImageDark = AppConfig.welcomeImageDark + upPreferenceSummary(PreferKey.welcomeImage, welcomeImage) + upPreferenceSummary(PreferKey.welcomeImageDark, welcomeImageDark) + findPreference(PreferKey.welcomeShowText)?.let { + it.isEnabled = !welcomeImage.isNullOrEmpty() + } + findPreference(PreferKey.welcomeShowIcon)?.let { + it.isEnabled = !welcomeImage.isNullOrEmpty() + } + findPreference(PreferKey.welcomeShowTextDark)?.let { + it.isEnabled = !welcomeImageDark.isNullOrEmpty() + } + findPreference(PreferKey.welcomeShowIconDark)?.let { + it.isEnabled = !welcomeImageDark.isNullOrEmpty() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.setTitle(R.string.welcome_style) + listView.setEdgeEffectColor(primaryColor) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + sharedPreferences ?: return + when (key) { + PreferKey.welcomeImage -> { + val welcomeImage = getPrefString(key) + upPreferenceSummary(key, welcomeImage) + findPreference(PreferKey.welcomeShowText)?.let { + it.isEnabled = !welcomeImage.isNullOrEmpty() + } + findPreference(PreferKey.welcomeShowIcon)?.let { + it.isEnabled = !welcomeImage.isNullOrEmpty() + } + } + + PreferKey.welcomeImageDark -> { + val welcomeImageDark = getPrefString(key) + upPreferenceSummary(key, welcomeImageDark) + findPreference(PreferKey.welcomeShowTextDark)?.let { + it.isEnabled = !welcomeImageDark.isNullOrEmpty() + } + findPreference(PreferKey.welcomeShowIconDark)?.let { + it.isEnabled = !welcomeImageDark.isNullOrEmpty() + } + } + } + } + + @SuppressLint("PrivateResource") + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + PreferKey.welcomeImage -> + if (getPrefString(preference.key).isNullOrEmpty()) { + selectImage.launch { + requestCode = requestWelcomeImage + mode = HandleFileContract.IMAGE + } + } else { + context?.selector( + items = arrayListOf( + getString(R.string.delete), + getString(R.string.select_image) + ) + ) { _, i -> + if (i == 0) { + removePref(preference.key) + AppConfig.welcomeShowText = true + AppConfig.welcomeShowIcon = true + findPreference(PreferKey.welcomeShowText)?.let { + it.isChecked = true + } + findPreference(PreferKey.welcomeShowIcon)?.let { + it.isChecked = true + } + BookCover.upDefaultCover() + } else { + selectImage.launch { + requestCode = requestWelcomeImage + mode = HandleFileContract.IMAGE + } + } + } + } + + PreferKey.welcomeImageDark -> + if (getPrefString(preference.key).isNullOrEmpty()) { + selectImage.launch { + requestCode = requestWelcomeImageDark + mode = HandleFileContract.IMAGE + } + } else { + context?.selector( + items = arrayListOf( + getString(R.string.delete), + getString(R.string.select_image) + ) + ) { _, i -> + if (i == 0) { + removePref(preference.key) + AppConfig.welcomeShowTextDark = true + AppConfig.welcomeShowIconDark = true + findPreference(PreferKey.welcomeShowTextDark)?.let { + it.isChecked = true + } + findPreference(PreferKey.welcomeShowIconDark)?.let { + it.isChecked = true + } + BookCover.upDefaultCover() + } else { + selectImage.launch { + requestCode = requestWelcomeImageDark + mode = HandleFileContract.IMAGE + } + } + } + } + } + return super.onPreferenceTreeClick(preference) + } + + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.welcomeImage, + PreferKey.welcomeImageDark -> preference.summary = if (value.isNullOrBlank()) { + getString(R.string.select_image) + } else { + value + } + + else -> preference.summary = value + } + } + + private fun setCoverFromUri(preferenceKey: String, uri: Uri) { + readUri(uri) { fileDoc, inputStream -> + kotlin.runCatching { + var file = requireContext().externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val fileName = uri.inputStream(requireContext()).getOrThrow().use { + MD5Utils.md5Encode(it) + ".$suffix" + } + file = FileUtils.createFileIfNotExist(file, "covers", fileName) + FileOutputStream(file).use { + inputStream.copyTo(it) + } + putPrefString(preferenceKey, file.absolutePath) + }.onFailure { + appCtx.toastOnUi(it.localizedMessage) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt new file mode 100755 index 000000000..1f571d86b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt @@ -0,0 +1,91 @@ +package io.legado.app.ui.dict + +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import com.google.android.material.tabs.TabLayout +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.entities.DictRule +import io.legado.app.databinding.DialogDictBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.utils.setHtml +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 词典 + */ +class DictDialog() : BaseDialogFragment(R.layout.dialog_dict) { + + constructor(word: String) : this() { + arguments = Bundle().apply { + putString("word", word) + } + } + + private val viewModel by viewModels() + private val binding by viewBinding(DialogDictBinding::bind) + + private var word: String? = null + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.tvDict.movementMethod = LinkMovementMethod() + word = arguments?.getString("word") + if (word.isNullOrEmpty()) { + toastOnUi(R.string.cannot_empty) + dismiss() + return + } + binding.tabLayout.setBackgroundColor(backgroundColor) + binding.tabLayout.setSelectedTabIndicatorColor(accentColor) + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab) { + + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + + } + + override fun onTabSelected(tab: TabLayout.Tab) { + val dictRule = tab.tag as DictRule + binding.rotateLoading.visible() + viewModel.dict(dictRule, word!!) { + binding.rotateLoading.inVisible() + binding.tvDict.setHtml(it) + } + } + }) + viewModel.initData { + it.forEach { + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + text = it.name + tag = it + }) + } + + setupTabLayoutMode(it.size) + } + } + + //根据已启用词典数动态选取布局 + private fun setupTabLayoutMode(dictCount: Int) { + if (dictCount <= 4) { + binding.tabLayout.tabMode = TabLayout.MODE_FIXED + binding.tabLayout.tabGravity = TabLayout.GRAVITY_FILL + } else { + binding.tabLayout.tabMode = TabLayout.MODE_SCROLLABLE + binding.tabLayout.tabGravity = TabLayout.GRAVITY_CENTER + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt new file mode 100755 index 000000000..f8c56d3aa --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt @@ -0,0 +1,37 @@ +package io.legado.app.ui.dict + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.help.coroutine.Coroutine + +class DictViewModel(application: Application) : BaseViewModel(application) { + + private var dictJob: Coroutine? = null + + fun initData(onSuccess: (List) -> Unit) { + execute { + appDb.dictRuleDao.enabled + }.onSuccess { + onSuccess.invoke(it) + } + } + + fun dict( + dictRule: DictRule, + word: String, + onFinally: (String) -> Unit + ) { + dictJob?.cancel() + dictJob = execute { + dictRule.search(word) + }.onSuccess { + onFinally.invoke(it) + }.onError { + onFinally.invoke(it.localizedMessage ?: "ERROR") + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt new file mode 100755 index 000000000..a9f73d6a9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleActivity.kt @@ -0,0 +1,241 @@ +package io.legado.app.ui.dict.rule + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.databinding.ActivityDictRuleBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.association.ImportDictRuleDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ACache +import io.legado.app.utils.GSON +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.launch +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +class DictRuleActivity : VMBaseActivity(), + PopupMenu.OnMenuItemClickListener, + SelectActionBar.CallBack, + DictRuleAdapter.CallBack { + + override val viewModel by viewModels() + override val binding by viewBinding(ActivityDictRuleBinding::inflate) + private val importRecordKey = "dictRuleUrls" + private val adapter by lazy { DictRuleAdapter(this, this) } + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + showDialogFragment(ImportDictRuleDialog(it)) + } + private val importDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportDictRuleDialog(uri.toString())) + } + } + private val exportResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + sendToClip(uri.toString()) + } + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initRecyclerView() + initSelectActionView() + observeDictRuleData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.dict_rule, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + // When this page is opened, it is in selection mode + dragSelectTouchHelper.activeSlideSelect() + + // Note: need judge selection first, so add ItemTouchHelper after it. + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + + private fun initSelectActionView() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.dict_rule_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun observeDictRuleData() { + lifecycleScope.launch { + appDb.dictRuleDao.flowAll().catch { + AppLog.put("字典规则获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it, adapter.diffItemCallBack) + } + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add -> showDialogFragment() + R.id.menu_import_local -> importDoc.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_qr -> qrCodeResult.launch() + R.id.menu_import_default -> viewModel.importDefault() + R.id.menu_help -> showHelp("dictRuleHelp") + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_enable_selection -> { + viewModel.enableSelection(*adapter.selection.toTypedArray()) + } + + R.id.menu_disable_selection -> { + viewModel.disableSelection(*adapter.selection.toTypedArray()) + } + + R.id.menu_export_selection -> exportResult.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "exportDictRule.json", + GSON.toJson(adapter.selection).toByteArray(), + "application/json" + ) + } + } + return true + } + + override fun onClickSelectBarMainAction() { + viewModel.delete(*adapter.selection.toTypedArray()) + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun update(vararg rule: DictRule) { + viewModel.update(*rule) + } + + override fun delete(rule: DictRule) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + rule.name) + noButton() + yesButton { + viewModel.delete(rule) + } + } + } + + override fun edit(rule: DictRule) { + showDialogFragment(DictRuleEditDialog(rule.name)) + } + + override fun upOrder() { + viewModel.upSortNumber() + } + + override fun upCountView() { + binding.selectActionBar.upCountView( + adapter.selection.size, + adapter.itemCount + ) + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(importRecordKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (it.isAbsUrl() && !cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + showDialogFragment( + ImportDictRuleDialog(it) + ) + } + } + cancelButton() + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleAdapter.kt new file mode 100755 index 000000000..939fdb481 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleAdapter.kt @@ -0,0 +1,208 @@ +package io.legado.app.ui.dict.rule + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.DictRule +import io.legado.app.databinding.ItemDictRuleBinding +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.ColorUtils + + +class DictRuleAdapter(context: Context, var callBack: CallBack) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private val selected = linkedSetOf() + + val selection: List + get() { + return getItems().filter { + selected.contains(it) + } + } + + val diffItemCallBack = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: DictRule, newItem: DictRule): Boolean { + return oldItem.name == newItem.name + } + + override fun areContentsTheSame(oldItem: DictRule, newItem: DictRule): Boolean { + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.enabled != newItem.enabled) { + return false + } + return true + } + + override fun getChangePayload(oldItem: DictRule, newItem: DictRule): Any? { + val payload = Bundle() + if (oldItem.name != newItem.name) { + payload.putBoolean("upName", true) + } + if (oldItem.enabled != newItem.enabled) { + payload.putBoolean("enabled", newItem.enabled) + } + if (payload.isEmpty) { + return null + } + return payload + } + } + + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + override fun getViewBinding(parent: ViewGroup): ItemDictRuleBinding { + return ItemDictRuleBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { + callBack.upCountView() + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemDictRuleBinding, + item: DictRule, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) + cbName.text = item.name + swtEnabled.isChecked = item.enabled + cbName.isChecked = selected.contains(item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "selected" -> cbName.isChecked = selected.contains(item) + "upName" -> cbName.text = item.name + "enabled" -> swtEnabled.isChecked = item.enabled + } + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemDictRuleBinding) { + binding.apply { + swtEnabled.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + it.enabled = isChecked + callBack.update(it) + } + } + cbName.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (cbName.isChecked) { + selected.add(it) + } else { + selected.remove(it) + } + } + callBack.upCountView() + } + ivEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) + } + } + ivDelete.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.delete(it) + } + } + } + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.sortNumber == targetItem.sortNumber) { + callBack.upOrder() + } else { + val srcOrder = srcItem.sortNumber + srcItem.sortNumber = targetItem.sortNumber + targetItem.sortNumber = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = linkedSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() + } + } + + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selected + } + + override fun getItemId(position: Int): DictRule { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upCountView() + return true + } + return false + } + } + + interface CallBack { + fun update(vararg rule: DictRule) + fun delete(rule: DictRule) + fun edit(rule: DictRule) + fun upOrder() + fun upCountView() + } +} diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleEditDialog.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleEditDialog.kt new file mode 100755 index 000000000..5b5c987ee --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleEditDialog.kt @@ -0,0 +1,122 @@ +package io.legado.app.ui.dict.rule + +import android.app.Application +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar + +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.databinding.DialogDictRuleEditBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class DictRuleEditDialog() : BaseDialogFragment(R.layout.dialog_dict_rule_edit, true), + Toolbar.OnMenuItemClickListener { + + val viewModel by viewModels() + val binding by viewBinding(DialogDictRuleEditBinding::bind) + + constructor(name: String) : this() { + arguments = Bundle().apply { + putString("name", name) + } + } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.inflateMenu(R.menu.dict_rule_edit) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + viewModel.initData(arguments?.getString("name")) { + upRuleView(viewModel.dictRule) + } + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> viewModel.save(getDictRule()) { + dismissAllowingStateLoss() + } + R.id.menu_copy_rule -> viewModel.copyRule(getDictRule()) + R.id.menu_paste_rule -> viewModel.pasteRule { + upRuleView(it) + } + } + return true + } + + private fun upRuleView(dictRule: DictRule?) { + binding.tvRuleName.setText(dictRule?.name) + binding.tvUrlRule.setText(dictRule?.urlRule) + binding.tvShowRule.setText(dictRule?.showRule) + } + + private fun getDictRule(): DictRule { + val dictRule = viewModel.dictRule?.copy() ?: DictRule() + dictRule.name = binding.tvRuleName.text.toString() + dictRule.urlRule = binding.tvUrlRule.text.toString() + dictRule.showRule = binding.tvShowRule.text.toString() + return dictRule + } + + class DictRuleEditViewModel(application: Application) : BaseViewModel(application) { + + var dictRule: DictRule? = null + + fun initData(name: String?, onFinally: () -> Unit) { + execute { + if (dictRule == null && name != null) { + dictRule = appDb.dictRuleDao.getByName(name) + } + }.onFinally { + onFinally.invoke() + } + } + + fun save(newDictRule: DictRule, onFinally: () -> Unit) { + execute { + dictRule?.let { + appDb.dictRuleDao.delete(it) + } + appDb.dictRuleDao.insert(newDictRule) + dictRule = newDictRule + }.onFinally { + onFinally.invoke() + } + } + + fun copyRule(dictRule: DictRule) { + context.sendToClip(GSON.toJson(dictRule)) + } + + fun pasteRule(success: (DictRule) -> Unit) { + val text = context.getClipText() + if (text.isNullOrBlank()) { + context.toastOnUi("剪贴板没有内容") + return + } + execute { + GSON.fromJsonObject(text).getOrThrow() + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi("格式不对") + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt new file mode 100755 index 000000000..e3f8bdbe1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleViewModel.kt @@ -0,0 +1,64 @@ +package io.legado.app.ui.dict.rule + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.DictRule +import io.legado.app.help.DefaultData +import io.legado.app.utils.toastOnUi + +class DictRuleViewModel(application: Application) : BaseViewModel(application) { + + + fun update(vararg dictRule: DictRule) { + execute { + appDb.dictRuleDao.update(*dictRule) + }.onError { + val msg = "更新字典规则出错\n${it.localizedMessage}" + AppLog.put(msg, it) + context.toastOnUi(msg) + } + } + + fun delete(vararg dictRule: DictRule) { + execute { + appDb.dictRuleDao.delete(*dictRule) + }.onError { + val msg = "删除字典规则出错\n${it.localizedMessage}" + AppLog.put(msg, it) + context.toastOnUi(msg) + } + } + + fun upSortNumber() { + execute { + val rules = appDb.dictRuleDao.all + for ((index, rule) in rules.withIndex()) { + rule.sortNumber = index + 1 + } + appDb.dictRuleDao.insert(*rules.toTypedArray()) + } + } + + fun enableSelection(vararg dictRule: DictRule) { + execute { + val array = dictRule.map { it.copy(enabled = true) }.toTypedArray() + appDb.dictRuleDao.insert(*array) + } + } + + fun disableSelection(vararg dictRule: DictRule) { + execute { + val array = dictRule.map { it.copy(enabled = false) }.toTypedArray() + appDb.dictRuleDao.insert(*array) + } + } + + fun importDefault() { + execute { + DefaultData.importDefaultDictRules() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/FileManageActivity.kt b/app/src/main/java/io/legado/app/ui/file/FileManageActivity.kt new file mode 100755 index 000000000..56301e775 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/FileManageActivity.kt @@ -0,0 +1,232 @@ +package io.legado.app.ui.file + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.core.content.FileProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppConst +import io.legado.app.databinding.ActivityFileManageBinding +import io.legado.app.databinding.ItemFileBinding +import io.legado.app.databinding.ItemPathPickerBinding +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.file.utils.FilePickerIcon +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ConvertUtils +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.applyTint +import io.legado.app.utils.openFileUri +import io.legado.app.utils.viewbindingdelegate.viewBinding +import java.io.File + +class FileManageActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivityFileManageBinding::inflate) + override val viewModel by viewModels() + private val dirParent = ".." + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private val pathAdapter by lazy { + PathAdapter() + } + private val fileAdapter by lazy { + FileAdapter() + } + private val currentFiles = arrayListOf() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initSearchView() + viewModel.upFiles(viewModel.rootDoc) + } + + private fun initView() { + binding.rvPath.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) + binding.rvPath.adapter = pathAdapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = fileAdapter + binding.recyclerView.applyNavigationBarPadding() + onBackPressedDispatcher.addCallback(this) { + if (viewModel.lastDir != viewModel.rootDoc) { + gotoLastDir() + return@addCallback + } + finish() + } + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.queryHint = getString(R.string.screen) + " • " + getString(R.string.file_manage) + searchView.isSubmitButtonEnabled = true + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + updateFiles() + return false + } + }) + } + + private fun updateFiles() { + if (searchView.query.isNotEmpty()) { + currentFiles.filter { + it.name == dirParent || it.name.contains(searchView.query) + }.let { + fileAdapter.setItems(it) + } + } else { + fileAdapter.setItems(currentFiles) + } + } + + private fun gotoLastDir() { + viewModel.subDocs.removeLastOrNull() + pathAdapter.setItems(viewModel.subDocs) + viewModel.upFiles(viewModel.lastDir) + } + + override fun observeLiveBus() { + viewModel.filesLiveData.observe(this) { + searchView.setQuery("", false) + currentFiles.clear() + currentFiles.addAll(it) + updateFiles() + } + } + + @SuppressLint("SetTextI18n") + inner class PathAdapter : + RecyclerAdapter(this@FileManageActivity) { + + private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getArrow()) + + init { + addHeaderView { + ItemPathPickerBinding.inflate(inflater, it, false).apply { + textView.text = "root" + imageView.setImageDrawable(arrowIcon) + root.setOnClickListener { + viewModel.subDocs.clear() + setItems(viewModel.subDocs) + viewModel.upFiles(viewModel.rootDoc) + } + } + } + } + + override fun getViewBinding(parent: ViewGroup): ItemPathPickerBinding { + return ItemPathPickerBinding.inflate(inflater, parent, false).apply { + imageView.setImageDrawable(arrowIcon) + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemPathPickerBinding) { + binding.root.setOnClickListener { + viewModel.subDocs = viewModel.subDocs.subList(0, holder.layoutPosition) + setItems(viewModel.subDocs) + viewModel.upFiles(viewModel.subDocs.lastOrNull()) + } + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemPathPickerBinding, + item: File, + payloads: MutableList + ) { + binding.textView.text = item.name + } + + } + + inner class FileAdapter : RecyclerAdapter(this@FileManageActivity) { + private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUpDir())!! + private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFolder())!! + private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFile())!! + + override fun getViewBinding(parent: ViewGroup): ItemFileBinding { + return ItemFileBinding.inflate(inflater, parent, false) + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFileBinding) { + binding.root.setOnClickListener { + val item = getItemByLayoutPosition(holder.layoutPosition) + item?.let { + if (item == viewModel.lastDir) { + gotoLastDir() + } else if (item.isDirectory) { + viewModel.subDocs.add(item) + pathAdapter.setItems(viewModel.subDocs) + viewModel.upFiles(item) + } else { + openFileUri( + FileProvider.getUriForFile( + this@FileManageActivity, + AppConst.authority, + item + ) + ) + } + } + } + binding.root.setOnLongClickListener { view -> + val item = getItemByLayoutPosition(holder.layoutPosition) + if (item == viewModel.lastDir) { + return@setOnLongClickListener true + } + item?.let { + showFileMenu(view, item) + } + return@setOnLongClickListener true + } + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFileBinding, + item: File, + payloads: MutableList + ) { + if (item == viewModel.lastDir) { + binding.imageView.setImageDrawable(upIcon) + binding.textView.text = dirParent + } else if (item.isDirectory) { + binding.imageView.setImageDrawable(folderIcon) + binding.textView.text = item.name + } else { + binding.imageView.setImageDrawable(fileIcon) + binding.textView.text = item.name + } + } + + private fun showFileMenu(view: View, file: File) { + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.file_long_click) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_del -> viewModel.delFile(file) + } + true + } + popupMenu.show() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/FileManageViewModel.kt b/app/src/main/java/io/legado/app/ui/file/FileManageViewModel.kt new file mode 100755 index 000000000..5720f373d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/FileManageViewModel.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.file + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.utils.toastOnUi +import java.io.File + +class FileManageViewModel(application: Application) : BaseViewModel(application) { + + val rootDoc = context.getExternalFilesDir(null)?.parentFile + var subDocs = mutableListOf() + val filesLiveData = MutableLiveData>() + + val lastDir: File? get() = subDocs.lastOrNull() ?: rootDoc + + fun upFiles(parentFile: File?) { + execute { + parentFile ?: return@execute emptyList() + if (parentFile == rootDoc) { + parentFile.listFiles()?.sortedWith( + compareBy({ it.isFile }, { it.name }) + ) + } else { + val list = arrayListOf(parentFile) + parentFile.listFiles()?.sortedWith( + compareBy({ it.isFile }, { it.name }) + )?.let { + list.addAll(it) + } + list + } + }.onStart { + filesLiveData.postValue(emptyList()) + }.onSuccess { + filesLiveData.postValue(it ?: emptyList()) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + + fun delFile(file: File) { + execute { + file.delete() + }.onSuccess { + upFiles(lastDir) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/FilePickerDialog.kt b/app/src/main/java/io/legado/app/ui/file/FilePickerDialog.kt new file mode 100755 index 000000000..8364ecf25 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/FilePickerDialog.kt @@ -0,0 +1,295 @@ +package io.legado.app.ui.file + +import android.annotation.SuppressLint +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogFileChooserBinding +import io.legado.app.databinding.ItemFilePickerBinding +import io.legado.app.databinding.ItemPathPickerBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.getPrimaryDisabledTextColor +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.file.HandleFileContract.Companion.FILE +import io.legado.app.ui.file.utils.FilePickerIcon +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ConvertUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.applyTint +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import java.io.File + + +class FilePickerDialog : BaseDialogFragment(R.layout.dialog_file_chooser), + Toolbar.OnMenuItemClickListener { + + companion object { + const val tag = "FileChooserDialog" + + fun show( + manager: FragmentManager, + mode: Int = FILE, + title: String? = null, + initPath: String? = null, + isShowHideDir: Boolean = false, + allowExtensions: Array? = null, + ) { + FilePickerDialog().apply { + val bundle = Bundle() + bundle.putInt("mode", mode) + bundle.putString("title", title) + bundle.putBoolean("isShowHideDir", isShowHideDir) + bundle.putString("initPath", initPath) + bundle.putStringArray("allowExtensions", allowExtensions) + arguments = bundle + }.show(manager, tag) + } + } + + private val binding by viewBinding(DialogFileChooserBinding::bind) + private val viewModel by viewModels() + private val dirParent = ".." + private val pathAdapter by lazy { PathAdapter() } + private val fileAdapter by lazy { FileAdapter() } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.8f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + view.setBackgroundResource(R.color.background_card) + initMenu() + initContentView() + viewModel.filesLiveData.observe(viewLifecycleOwner) { + fileAdapter.selectFile = null + fileAdapter.setItems(it) + } + viewModel.initData(arguments) + binding.toolBar.title = arguments?.getString("title") ?: let { + if (viewModel.isSelectDir) { + getString(R.string.folder_chooser) + } else { + getString(R.string.file_chooser) + } + } + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.file_chooser) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + } + + private fun initContentView() { + binding.rvPath.layoutManager = LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false) + binding.rvPath.adapter = pathAdapter + + binding.rvFile.addItemDecoration(VerticalDivider(requireContext())) + binding.rvFile.layoutManager = LinearLayoutManager(activity) + binding.rvFile.adapter = fileAdapter + + binding.tvOk.setOnClickListener { + if (viewModel.isSelectDir) { + viewModel.lastDir?.let { + setResultData(it.path) + dismissAllowingStateLoss() + } + } else { + val file = fileAdapter.selectFile + if (file == null) { + toastOnUi("请选择文件") + } else { + setResultData(file.path) + dismissAllowingStateLoss() + } + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_create -> alert(R.string.create_folder) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "文件夹名" + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + if (text.isNullOrBlank()) { + toastOnUi("文件夹名不能为空") + } else { + viewModel.createFolder(text.trim()) + } + } + cancelButton() + } + } + return true + } + + private fun setResultData(path: String) { + val data = Intent().setData(Uri.fromFile(File(path))) + (parentFragment as? CallBack)?.onResult(data) + (activity as? CallBack)?.onResult(data) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + activity?.finish() + } + + @SuppressLint("SetTextI18n") + inner class PathAdapter : + RecyclerAdapter(requireContext()) { + + private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getArrow()) + + init { + addHeaderView { + ItemPathPickerBinding.inflate(inflater, it, false).apply { + textView.text = "root" + imageView.setImageDrawable(arrowIcon) + root.setOnClickListener { + viewModel.subDocs.clear() + setItems(viewModel.subDocs) + viewModel.upFiles(viewModel.rootDoc) + } + } + } + } + + override fun getViewBinding(parent: ViewGroup): ItemPathPickerBinding { + return ItemPathPickerBinding.inflate(inflater, parent, false).apply { + imageView.setImageDrawable(arrowIcon) + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemPathPickerBinding) { + binding.root.setOnClickListener { + viewModel.subDocs = viewModel.subDocs.subList(0, holder.layoutPosition) + setItems(viewModel.subDocs) + viewModel.upFiles(viewModel.subDocs.lastOrNull()) + } + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemPathPickerBinding, + item: File, + payloads: MutableList + ) { + binding.textView.text = item.name + } + + } + + inner class FileAdapter : RecyclerAdapter(requireContext()) { + private val primaryTextColor = context.getPrimaryTextColor(!AppConfig.isNightTheme) + private val disabledTextColor = context.getPrimaryDisabledTextColor(!AppConfig.isNightTheme) + private val upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUpDir())!! + private val folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFolder())!! + private val fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFile())!! + private val selectDrawable = + ResourcesCompat.getDrawable(resources, R.drawable.shape_radius_1dp, null)!!.apply { + DrawableCompat.setTint(this, primaryTextColor) + } + var selectFile: File? = null + + override fun getViewBinding(parent: ViewGroup): ItemFilePickerBinding { + return ItemFilePickerBinding.inflate(inflater, parent, false) + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFilePickerBinding) { + binding.root.setOnClickListener { + val item = getItemByLayoutPosition(holder.layoutPosition) + item?.let { + if (item == viewModel.lastDir) { + viewModel.subDocs.removeLastOrNull() + pathAdapter.setItems(viewModel.subDocs) + viewModel.upFiles(viewModel.subDocs.lastOrNull() ?: viewModel.rootDoc) + } else if (item.isDirectory) { + viewModel.subDocs.add(item) + pathAdapter.setItems(viewModel.subDocs) + viewModel.upFiles(item) + } else if (viewModel.isSelectFile) { + viewModel.allowExtensions.let { + if (it.isNullOrEmpty() || it.contains(FileUtils.getExtension(item.path))) { + selectFile = item + notifyItemRangeChanged(getHeaderCount(), itemCount, "selectFile") + } + } + } + } + } + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFilePickerBinding, + item: File, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + if (item == viewModel.lastDir) { + binding.imageView.setImageDrawable(upIcon) + binding.textView.text = dirParent + } else if (item.isDirectory) { + binding.imageView.setImageDrawable(folderIcon) + binding.textView.text = item.name + } else { + binding.imageView.setImageDrawable(fileIcon) + binding.textView.text = item.name + } + if (item.isDirectory) { + binding.textView.setTextColor(primaryTextColor) + } else { + if (viewModel.isSelectDir) { + binding.textView.setTextColor(disabledTextColor) + } else { + viewModel.allowExtensions?.let { + if (it.isEmpty() || it.contains(FileUtils.getExtension(item.path))) { + binding.textView.setTextColor(primaryTextColor) + } else { + binding.textView.setTextColor(disabledTextColor) + } + } ?: binding.textView.setTextColor(primaryTextColor) + } + } + } + binding.root.isSelected = item == selectFile + if (item == selectFile) { + binding.root.background = selectDrawable + } else { + binding.root.setBackgroundColor(getCompatColor(R.color.transparent)) + } + } + + } + + interface CallBack { + fun onResult(data: Intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/FilePickerViewModel.kt b/app/src/main/java/io/legado/app/ui/file/FilePickerViewModel.kt new file mode 100755 index 000000000..a480f04c0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/FilePickerViewModel.kt @@ -0,0 +1,76 @@ +package io.legado.app.ui.file + +import android.app.Application +import android.os.Bundle +import android.os.Environment +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.exception.NoStackTraceException +import io.legado.app.utils.toastOnUi +import java.io.File + +class FilePickerViewModel(application: Application) : BaseViewModel(application) { + + var rootDoc: File? = Environment.getExternalStorageDirectory() + var subDocs = mutableListOf() + val filesLiveData = MutableLiveData>() + var mode: Int = HandleFileContract.FILE + var isShowHideDir: Boolean = false + var allowExtensions: Array? = null + val isSelectDir: Boolean get() = mode == HandleFileContract.DIR + val isSelectFile: Boolean get() = mode == HandleFileContract.FILE + val lastDir: File? get() = subDocs.lastOrNull() ?: rootDoc + + fun initData(arguments: Bundle?) { + arguments?.let { + mode = it.getInt("mode", HandleFileContract.FILE) + isShowHideDir = it.getBoolean("isShowHideDir") + it.getString("initPath")?.let { path -> + rootDoc = File(path) + } + allowExtensions = it.getStringArray("allowExtensions") + } + upFiles(rootDoc) + } + + fun upFiles(parentFile: File?) { + execute { + parentFile ?: return@execute emptyList() + if (parentFile == rootDoc) { + parentFile.listFiles()?.sortedWith( + compareBy({ it.isFile }, { it.name }) + ) + } else { + val list = arrayListOf(parentFile) + parentFile.listFiles()?.sortedWith( + compareBy({ it.isFile }, { it.name }) + )?.let { + list.addAll(it) + } + list + } + }.onStart { + filesLiveData.postValue(emptyList()) + }.onSuccess { + filesLiveData.postValue(it ?: emptyList()) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + + fun createFolder(name: String) { + execute { + val dir = lastDir ?: throw NoStackTraceException("父文件夹不存在") + val folder = File(dir, name) + if (!folder.canonicalPath.contains(dir.canonicalPath)) { + throw NoStackTraceException("非法文件名") + } + folder.mkdir() + }.onSuccess { + upFiles(lastDir) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/HandleFileActivity.kt b/app/src/main/java/io/legado/app/ui/file/HandleFileActivity.kt new file mode 100755 index 000000000..211f9b9e6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/HandleFileActivity.kt @@ -0,0 +1,331 @@ +package io.legado.app.ui.file + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.webkit.MimeTypeMap +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.databinding.ActivityTranslucenceBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.IntentData +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.utils.SelectImageContract +import io.legado.app.utils.checkWrite +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getJsonArray +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.launch +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import splitties.init.appCtx +import java.io.File + +class HandleFileActivity : + VMBaseActivity(), + FilePickerDialog.CallBack { + + override val binding by viewBinding(ActivityTranslucenceBinding::inflate) + override val viewModel by viewModels() + private var mode = 0 + + private val selectDocTree = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + uri?.let { + if (uri.isContentScheme()) { + val modeFlags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + contentResolver.takePersistableUriPermission(uri, modeFlags) + } + onResult(Intent().setData(uri)) + } ?: finish() + } + + private val selectDoc = registerForActivityResult(ActivityResultContracts.OpenDocument()) { + it?.let { + if (it.isContentScheme()) { + val modeFlags = + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + contentResolver.takePersistableUriPermission(it, modeFlags) + } + onResult(Intent().setData(it)) + } ?: finish() + } + + private val selectImage = registerForActivityResult(SelectImageContract()) { + it.uri?.let { uri -> + onResult(Intent().setData(uri)) + } ?: finish() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + mode = intent.getIntExtra("mode", 0) + viewModel.errorLiveData.observe(this) { + toastOnUi(it) + finish() + } + val allowExtensions = intent.getStringArrayExtra("allowExtensions") + val selectList: ArrayList> = when (mode) { + HandleFileContract.DIR_SYS -> getDirActions(true) + HandleFileContract.DIR -> getDirActions() + HandleFileContract.FILE -> getFileActions() + HandleFileContract.EXPORT -> arrayListOf( + SelectItem(getString(R.string.upload_url), 111) + ).apply { + addAll(getDirActions()) + } + + HandleFileContract.IMAGE -> getImageActions() + else -> arrayListOf() + } + intent.getJsonArray>("otherActions")?.let { + selectList.addAll(it) + } + val title = intent.getStringExtra("title") ?: let { + when (mode) { + HandleFileContract.EXPORT -> return@let getString(R.string.export) + HandleFileContract.DIR -> return@let getString(R.string.select_folder) + HandleFileContract.IMAGE -> return@let getString(R.string.select_image) + else -> return@let getString(R.string.select_file) + } + } + alert(title) { + items(selectList) { _, item, _ -> + when (item.value) { + HandleFileContract.DIR -> kotlin.runCatching { + selectDocTree.launch() + }.onFailure { + AppLog.put(getString(R.string.open_sys_dir_picker_error), it, true) + checkPermissions { + FilePickerDialog.show( + supportFragmentManager, + mode = HandleFileContract.DIR + ) + } + } + + HandleFileContract.FILE -> kotlin.runCatching { + selectDoc.launch(typesOfExtensions(allowExtensions)) + }.onFailure { + AppLog.put(getString(R.string.open_sys_dir_picker_error), it, true) + checkPermissions { + FilePickerDialog.show( + supportFragmentManager, + mode = HandleFileContract.FILE, + allowExtensions = allowExtensions + ) + } + } + + HandleFileContract.IMAGE -> { + selectImage.launch() + } + + 10 -> checkPermissions { + @Suppress("DEPRECATION") + lifecycleScope.launchWhenResumed { + FilePickerDialog.show( + supportFragmentManager, + mode = HandleFileContract.DIR + ) + } + } + + 11 -> checkPermissions { + @Suppress("DEPRECATION") + lifecycleScope.launchWhenResumed { + FilePickerDialog.show( + supportFragmentManager, + mode = HandleFileContract.FILE, + allowExtensions = allowExtensions + ) + } + } + + 111 -> getFileData()?.let { + viewModel.upload(it.first, it.second, it.third) { url -> + val uri = url.toUri() + setResult(RESULT_OK, Intent().setData(uri)) + finish() + } + } + + 112 -> checkPermissions { // 手动输入目录路径 + showInputDirectoryDialog() + } + + else -> { + val path = item.title + val uri = if (path.isContentScheme()) { + path.toUri() + } else { + Uri.fromFile(File(path)) + } + onResult(Intent().setData(uri)) + } + } + } + onCancelled { + finish() + } + } + } + + private fun showInputDirectoryDialog() { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.enter_directory_path) + } + + alert(getString(R.string.manual_input)) { + customView { alertBinding.root } + okButton { + val inputPath = alertBinding.editView.text.toString() + if (inputPath.isBlank()) { + toastOnUi(getString(R.string.empty_directory_input)) + return@okButton + } + val file = File(inputPath) + if (file.exists() && + file.isDirectory && + isExternalStorage(file) && + file.checkWrite() + ) { + onResult(Intent().setData(Uri.fromFile(file))) + } else { + toastOnUi(getString(R.string.invalid_directory)) + } + } + onDismiss { + finish() + } + cancelButton() + } + } + + private fun isExternalStorage(path: File): Boolean { + if (path.canonicalPath.startsWith(appCtx.externalFiles.parent!!)) { + return false + } + try { + if (Environment.isExternalStorageEmulated(path)) { + return true + } + } catch (_: IllegalArgumentException) { + } + try { + if (Environment.isExternalStorageRemovable(path)) { + return true + } + } catch (_: IllegalArgumentException) { + } + return false + } + + private fun getFileData(): Triple? { + val fileName = intent.getStringExtra("fileName") + val file = intent.getStringExtra("fileKey")?.let { + IntentData.get(it) + } + val contentType = intent.getStringExtra("contentType") + if (fileName != null && file != null && contentType != null) { + return Triple(fileName, file, contentType) + } + return null + } + + private fun getDirActions(onlySys: Boolean = false): ArrayList> { + return if (onlySys) { + arrayListOf( + SelectItem(getString(R.string.sys_folder_picker), HandleFileContract.DIR), + SelectItem(getString(R.string.manual_input), 112) // 添加手动输入选项 + ) + } else { + arrayListOf( + SelectItem(getString(R.string.sys_folder_picker), HandleFileContract.DIR), + SelectItem(getString(R.string.app_folder_picker), 10), + SelectItem(getString(R.string.manual_input), 112) // 添加手动输入选项 + ) + } + } + + private fun getFileActions(): ArrayList> { + return arrayListOf( + SelectItem(getString(R.string.sys_file_picker), HandleFileContract.FILE), + SelectItem(getString(R.string.app_file_picker), 11) + ) + } + + private fun getImageActions(): ArrayList> { + return arrayListOf( + SelectItem(getString(R.string.sys_image_picker), HandleFileContract.IMAGE) + ).apply { + addAll(getFileActions()) + } + } + + private fun checkPermissions(success: (() -> Unit)? = null) { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + success?.invoke() + } + .onDenied { + finish() + } + .onError { + finish() + } + .request() + } + + private fun typesOfExtensions(allowExtensions: Array?): Array { + val types = hashSetOf() + if (allowExtensions.isNullOrEmpty()) { + types.add("*/*") + } else { + allowExtensions.forEach { + when (it) { + "*" -> types.add("*/*") + "txt", "xml" -> types.add("text/*") + else -> { + val mime = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(it) + ?: "application/octet-stream" + types.add(mime) + } + } + } + } + return types.toTypedArray() + } + + override fun onResult(data: Intent) { + val uri = data.data + uri ?: let { + finish() + return + } + if (mode == HandleFileContract.EXPORT) { + getFileData()?.let { fileData -> + viewModel.saveToLocal(uri, fileData.first, fileData.second) { savedUri -> + setResult(RESULT_OK, Intent().setData(savedUri)) + finish() + } + } + } else { + data.putExtra("value", intent.getStringExtra("value")) + setResult(RESULT_OK, data) + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/HandleFileContract.kt b/app/src/main/java/io/legado/app/ui/file/HandleFileContract.kt new file mode 100755 index 000000000..b4251ca2a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/HandleFileContract.kt @@ -0,0 +1,89 @@ +package io.legado.app.ui.file + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract +import io.legado.app.help.IntentData +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.utils.RealPathUtil +import io.legado.app.utils.externalFiles +import io.legado.app.utils.putJson +import splitties.init.appCtx + +@Suppress("unused") +class HandleFileContract : + ActivityResultContract<(HandleFileContract.HandleFileParam.() -> Unit)?, HandleFileContract.Result>() { + + private var requestCode: Int = 0 + + override fun createIntent(context: Context, input: (HandleFileParam.() -> Unit)?): Intent { + val intent = Intent(context, HandleFileActivity::class.java) + val handleFileParam = HandleFileParam() + input?.let { + handleFileParam.apply(input) + } + if (handleFileParam.mode == IMAGE) { + handleFileParam.allowExtensions = arrayOf("jpg", "png", "bmp", "webp") + } + handleFileParam.let { + requestCode = it.requestCode + intent.putExtra("mode", it.mode) + intent.putExtra("title", it.title) + intent.putExtra("allowExtensions", it.allowExtensions) + intent.putJson("otherActions", it.otherActions) + it.fileData?.let { fileData -> + intent.putExtra("fileName", fileData.name) + intent.putExtra("fileKey", IntentData.put(fileData.data)) + intent.putExtra("contentType", fileData.type) + } + intent.putExtra("value", it.value) + } + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Result { + val uri = if (resultCode != RESULT_OK || intent?.data == null || + RealPathUtil.getTreePath(intent.data!!) + ?.startsWith(appCtx.externalFiles.parent!!) == true + ) { + null + } else { + intent.data + } + return Result(uri, requestCode, intent?.getStringExtra("value")) + } + + companion object { + const val DIR = 0 + const val FILE = 1 + const val DIR_SYS = 2 + const val EXPORT = 3 + const val IMAGE = 4 + } + + @Suppress("ArrayInDataClass") + data class HandleFileParam( + var mode: Int = DIR, + var title: String? = null, + var allowExtensions: Array = arrayOf(), + var otherActions: ArrayList>? = null, + var fileData: FileData? = null, + var requestCode: Int = 0, + var value: String? = null + ) + + data class Result( + val uri: Uri?, + val requestCode: Int, + val value: String? + ) + + data class FileData( + val name: String, + val data: Any, + val type: String + ) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/HandleFileViewModel.kt b/app/src/main/java/io/legado/app/ui/file/HandleFileViewModel.kt new file mode 100755 index 000000000..6eb110db1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/HandleFileViewModel.kt @@ -0,0 +1,63 @@ +package io.legado.app.ui.file + +import android.app.Application +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.help.DirectLinkUpload +import io.legado.app.utils.* + +import java.io.File + +class HandleFileViewModel(application: Application) : BaseViewModel(application) { + + val errorLiveData = MutableLiveData() + + fun upload( + fileName: String, + file: Any, + contentType: String, + success: (url: String) -> Unit + ) { + execute { + DirectLinkUpload.upLoad(fileName, file, contentType) + }.onSuccess { + success.invoke(it) + }.onError { + AppLog.put("上传文件失败\n${it.localizedMessage}", it) + it.printOnDebug() + errorLiveData.postValue(it.localizedMessage) + } + } + + fun saveToLocal(uri: Uri, fileName: String, data: Any, success: (uri: Uri) -> Unit) { + execute { + val bytes = when (data) { + is File -> data.readBytes() + is ByteArray -> data + is String -> data.toByteArray() + else -> GSON.toJson(data).toByteArray() + } + return@execute if (uri.isContentScheme()) { + val doc = DocumentFile.fromTreeUri(context, uri)!! + doc.findFile(fileName)?.delete() + val newDoc = doc.createFile("", fileName) + newDoc!!.writeBytes(context, bytes) + newDoc.uri + } else { + val file = File(uri.path ?: uri.toString()) + val newFile = FileUtils.createFileIfNotExist(file, fileName) + newFile.writeBytes(bytes) + Uri.fromFile(newFile) + } + }.onError { + it.printOnDebug() + errorLiveData.postValue(it.localizedMessage) + }.onSuccess { + success.invoke(it) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/file/utils/FilePickerIcon.java b/app/src/main/java/io/legado/app/ui/file/utils/FilePickerIcon.java new file mode 100755 index 000000000..138b2be3e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/file/utils/FilePickerIcon.java @@ -0,0 +1,283 @@ +package io.legado.app.ui.file.utils; + +/** + * Generated by https://github.com/gzu-liyujiang/Image2ByteVar + * + * @author 李玉江[QQ:1023694760] + * @since 2017/01/04 06:03 + */ +public class FilePickerIcon { + + public static byte[] getFile() { + return FILE; + } + + public static byte[] getFolder() { + return FOLDER; + } + + public static byte[] getHome() { + return HOME; + } + + public static byte[] getUpDir() { + return UPDIR; + } + + public static byte[] getArrow() { + return ARROW; + } + + // fixed: 17-1-7 "static final" arrays should be "private" + private static final byte[] FILE = { + -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, + 0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71, + 68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0, + 0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 1, -73, 73, 68, 65, 84, + 88, -123, -19, -106, 63, 75, 3, 49, 24, -121, 127, -74, 69, -37, 110, -30, -94, -109, -120, -126, + -125, -101, 56, 72, 63, -125, -97, -64, -63, 77, 28, 69, 20, 81, 68, 113, 83, 113, -12, 91, + -120, -96, -117, -120, -85, 31, 65, 29, 28, -100, -124, -94, -101, 46, -83, -105, 92, 114, 113, -24, + 31, -38, -110, 59, -34, -9, 114, 87, 69, -14, 64, 41, -92, 73, -34, -89, -55, -17, 114, 1, + 60, 30, -113, -25, 79, 50, 66, -19, 120, -7, 16, 26, 0, -112, 82, -61, 24, 3, 41, 35, + 0, -128, 20, 10, 74, 69, -48, 58, -126, 82, -90, -37, -90, -75, -127, 82, 81, 119, 124, 16, + 40, -100, 109, 79, -109, -21, 13, 82, -94, 118, -108, 82, 99, -91, 54, -58, 25, 2, 0, 120, + -85, 7, -72, -71, 127, -57, -18, -58, 12, -102, -51, 87, 115, 113, 56, -105, 74, -74, 64, -19, + 104, -116, 73, 51, 63, 0, -96, 88, 104, -107, 57, -34, -102, -59, -6, -63, 75, -86, -119, -56, + -94, -99, -83, -26, -96, 123, -122, 20, -37, -107, 78, -9, -25, -79, -74, -13, -52, -106, 37, -117, + 114, -23, 72, 42, 109, -96, -93, 8, 66, 0, -97, 95, -83, -49, -55, -34, 2, 86, 55, 31, + 89, -78, -12, -116, 10, -59, 18, -20, 48, 90, -86, -96, -47, -48, -72, -66, -5, 64, -40, -98, + -94, 90, -27, -81, 15, 89, -76, -9, 9, -114, 99, 80, 18, 0, -90, 38, -127, -38, -46, 4, + -76, 78, -97, 113, -128, 33, 42, -124, 78, -4, -35, 38, -87, -62, -42, -9, -14, -30, 120, 127, + -69, 6, -82, 110, -21, -44, -46, 0, -72, 103, 77, 12, 73, -110, 125, 109, -55, -1, 53, 17, + 86, 70, 109, 66, 113, -72, 72, -39, 32, -89, -38, 53, 99, -82, 100, -14, 48, -39, -74, 57, + 107, -100, -49, -47, -84, -77, 24, 7, 121, 69, -125, -64, 126, -114, 82, -91, -66, 3, 106, 37, + 59, -12, 21, 85, -46, -87, 80, -91, -20, 52, -100, 46, -38, -108, -87, 111, 104, -103, -64, 58, + 71, -121, -15, -48, -60, -63, -54, -24, -80, -14, 104, 35, -73, -37, 83, -42, -112, 69, 5, -15, + -10, -108, 23, -12, 3, 63, -92, -65, 63, 43, 101, -5, -10, 7, 14, -111, 112, -66, -108, -28, + -111, 71, 27, -1, 47, -93, -65, 77, 38, -9, 81, 27, 46, 121, -76, -63, 18, 29, 86, 30, + 109, -80, 68, -113, -50, -97, -14, -14, -16, 120, 60, -98, 1, 126, 0, -110, 81, -78, -5, 36, + 19, -64, 3, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; + private static final byte[] FOLDER = { + -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, + 0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71, + 68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0, + 0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -102, 73, 68, 65, 84, + 88, -123, -19, -105, 61, -113, 19, 49, 16, -122, 95, 111, -110, 75, -124, -124, -60, -49, 65, 20, + -48, 34, 81, 80, 32, 81, 83, 80, -94, 52, 72, 8, 78, 66, -94, 64, 20, -108, 124, 20, + 39, -47, 64, -51, -11, -7, 87, -96, 11, -38, -75, 61, 99, 15, -59, 110, 54, 118, -42, -5, + 113, -71, 77, 117, -5, 54, -55, -50, -38, -42, -77, -81, 61, 99, 27, -104, 52, 105, -46, -92, + 73, -73, 66, 106, 104, -61, -51, 57, -92, -21, -3, -29, 79, -61, -57, 58, 70, -13, 33, -115, + 54, -25, -112, 71, -17, 127, 0, 0, -106, -38, -105, 65, 93, 64, -56, 0, 108, 33, 76, -72, + -28, -113, -14, -20, -77, 59, 25, 108, 54, 4, -14, -63, -6, 59, -112, 123, -84, -4, -84, -114, + -117, 39, 96, -74, -17, -2, -12, -59, 91, 92, -66, -103, 117, -70, 126, 19, 117, 58, -80, 57, + -121, 60, 92, 127, 5, 0, -84, -106, -53, -3, 11, 93, -108, -96, 0, 96, 52, -124, 9, 96, + 6, 116, -127, -33, -65, -66, -44, -51, 56, -117, -121, -73, 62, 3, -7, 56, -74, 123, 126, -11, + -83, -24, 100, -23, -99, -6, -43, -67, -5, -119, -96, -119, -66, 80, 1, -128, 49, 0, -128, -25, + 31, -98, -108, 65, 103, -93, 46, -62, -36, 24, -58, -27, -37, -6, -65, -39, -66, -108, -41, 63, + -13, 86, -40, 65, 107, 84, -7, 43, 64, -103, 70, 92, 108, 17, -45, 82, -94, -115, -47, 77, + -40, -22, 35, -108, 53, 32, 34, 56, 34, 112, -49, -14, 30, 4, 90, 66, -2, -119, 99, -34, + 2, -69, -23, -33, -119, -10, -32, 18, -66, -37, -63, 114, 16, 99, -122, 2, -64, 87, 127, 97, + -56, -61, -53, 24, -96, -121, -14, -107, 35, 103, 11, -120, -91, 116, 27, -25, -45, -15, 96, 9, + 80, -95, 97, -56, -61, 114, 127, 14, -10, 102, 125, 27, 36, -128, 86, -56, -70, 34, -52, 50, + -128, 109, -78, 77, 40, 118, -82, -73, 77, -65, -93, 98, 26, -128, -111, -62, 41, 62, -101, 67, + 116, -111, 110, -41, 34, -53, 2, -26, 22, -9, 3, 29, 53, -11, -111, -109, -39, -94, -4, -83, + 0, 85, -74, -120, -41, -25, 72, -22, 4, -27, -86, -58, -119, 45, -102, -119, 3, 52, -36, -124, + 61, 40, 65, 81, -58, 55, -5, -117, 99, -80, 115, -89, 115, 52, 9, 93, 65, -90, -36, 76, + 65, 94, 87, -67, -96, 74, -52, -2, 52, -46, 54, -91, -95, -109, -119, 108, 87, -13, -59, 126, + -9, 74, 40, -109, 27, 58, 106, 124, 6, -79, 40, -117, -7, 33, 100, 0, 23, -71, -72, -37, + -1, -85, 105, 31, -61, 77, 96, -24, -44, -109, 1, 40, -19, 70, 50, 113, -126, -75, -39, -22, + -26, 53, -85, -61, 113, 89, 127, 8, 23, 78, 119, 80, 55, -5, -36, -12, 117, 34, -11, 23, + -4, 78, 80, -14, 10, 112, 22, -30, 92, -5, -6, 28, 2, 25, -70, -103, 112, -46, -75, -19, + 98, 67, 65, 57, 60, -110, -11, 13, 118, 4, -92, -9, 2, 79, 4, -94, 17, 118, 38, 97, + 6, -104, 64, -108, -34, -103, -124, -35, -63, 115, -20, -68, -46, 58, 124, 2, 0, 56, 91, -18, + 118, -123, -74, -48, 122, 88, -78, 117, -126, 90, 95, 102, -80, 49, 5, -88, 26, 80, -101, -46, + 33, 83, -24, -42, 126, 53, 116, 42, 97, -104, -22, 2, -97, 111, 115, 108, -13, 17, 64, 1, + -64, 85, 103, 71, 50, 84, 1, -106, 110, 88, 106, 95, 10, 93, -5, -67, -81, 62, -44, 22, + 26, 84, 1, -33, -67, -77, -24, 5, -19, -67, -116, 93, -84, 87, 2, 32, -70, 66, -104, -32, + -112, -53, 94, -63, -113, 112, 1, 125, 119, -15, -17, -92, -73, -40, 73, -109, 38, 77, -70, -19, + -6, 15, -2, -54, -98, -96, -19, -118, -95, -10, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, + 96, -126}; + private static final byte[] HOME = { + -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, + 0, 0, 0, 40, 8, 4, 0, 0, 0, 34, 2, -96, -37, 0, 0, 0, 1, 115, 82, 71, + 66, 0, -82, -50, 28, -23, 0, 0, 0, 2, 98, 75, 71, 68, 0, -1, -121, -113, -52, -65, + 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1, 120, -91, 63, + 118, 0, 0, 0, 7, 116, 73, 77, 69, 7, -37, 8, 4, 10, 36, 16, -22, -9, -18, -50, + 0, 0, 2, -84, 73, 68, 65, 84, 72, -57, -19, -106, 91, 72, 20, 97, 20, -57, 127, -77, + -18, -106, -41, 54, -14, -106, -23, -102, 4, 21, 97, 5, 5, 61, 70, 74, -76, -122, -94, -11, + 18, -108, -92, 121, 41, 16, -124, 96, -95, 48, -118, -108, -94, 18, -70, 96, 33, 68, -81, -127, + 15, 25, 68, 15, 21, 89, 80, 42, 25, 42, -91, 97, -106, -103, -122, 107, 41, -19, -86, -37, + -22, 122, -39, 77, -73, -103, -81, 7, -105, 84, -36, -85, -26, -125, -44, -127, -31, 59, -52, -103, + -7, 113, -50, -103, -17, -4, -65, -127, -1, -74, 44, 76, -102, 113, -29, 22, -119, 50, -3, -15, + 84, 75, -111, -87, -38, -81, -89, 84, 24, -120, 1, -96, -106, -102, -65, 83, -66, -118, 59, -119, + 39, 54, 97, 69, -31, -109, 115, -14, 8, 15, 124, -107, -17, 27, 42, 113, 123, 125, 81, 46, + 102, 28, 8, -84, -68, 116, 78, -26, 80, 77, 5, -79, 104, -48, -48, 70, -39, 124, -88, -38, + 103, 37, -107, -70, -94, 28, -6, 105, 7, 34, 89, 67, -86, -90, -74, 106, 82, -51, 65, 125, + -110, 13, 27, -99, 43, 2, -17, -87, -60, -51, -124, -30, 99, -104, -88, -63, -12, 20, -93, -90, + 96, 111, -80, -106, 20, 117, -3, -35, -97, -65, 54, -13, 29, 21, 8, -9, -3, -14, -122, -68, + 17, 127, 50, 15, 51, -49, 49, 85, -109, 73, -79, 51, -81, 94, -103, 96, 21, 123, -126, 66, + 86, 10, 64, 4, 12, -107, -72, -70, -50, -112, -57, 0, 47, -24, 123, 66, 46, 50, 80, -19, + 40, 121, -59, 20, -31, -20, 102, 0, 121, 1, -48, -14, -72, 83, -7, 12, 81, -121, -79, -106, + 67, 76, -71, -18, 94, -73, 85, 54, 34, 19, -122, 115, -102, 23, 16, -12, 114, 92, 73, 1, + 22, 26, -24, 110, -26, 0, -114, 89, 17, -61, -32, -61, 22, 4, 2, -127, -30, -31, 125, -9, + -48, -117, -79, 103, -13, -79, -48, 68, -57, 123, -46, 25, -101, 19, -109, 57, -38, -41, -40, -127, + 64, 16, -127, 70, -113, -34, 63, 104, 105, -52, -7, 2, -84, -76, -48, -42, 69, 26, -42, 121, + 113, 59, 89, 93, -35, -67, 8, -126, -39, -87, 81, -35, 103, -69, 111, -24, -71, -24, 11, -123, + 12, -45, -58, -37, -81, -20, -61, -20, -74, 18, 11, -23, -19, -125, 102, 20, -76, 36, 107, 121, + 76, -68, 119, -24, -103, -88, 75, -123, -116, -16, -111, 38, 51, 122, -66, 121, -4, -116, 95, 68, + -42, 59, -5, 8, -126, 24, 54, -24, 120, 68, -124, 103, -24, -23, -88, -14, -29, -40, -24, -28, + -75, 85, -92, -47, -27, 117, 48, -102, -27, -20, 86, 121, 28, 5, 29, 107, 119, 112, 111, -10, + 24, 5, -51, -72, 17, -122, -32, 107, -39, -110, -99, 30, -22, -58, -108, -3, -76, -6, 20, -93, + -49, -78, -59, -102, 17, -50, 40, -126, -47, -115, 66, 59, 94, -29, 78, 80, -86, -40, 74, -108, + 20, -119, 34, 50, -88, -13, 83, 58, -81, -112, 73, 47, 70, -116, 52, -104, -34, 120, 86, 41, + -119, 4, 10, 1, 24, -26, -106, 71, 88, 10, 41, 0, 60, -93, -47, 31, -107, 18, -124, -123, + -106, -19, -30, 7, 31, 122, -68, 64, 83, 19, 75, -75, -12, 51, 108, -101, -127, -6, -40, -4, + 97, -92, -110, -20, 67, 18, -109, -40, -58, 106, -113, -86, -66, 96, 83, -36, 15, -2, 34, -96, + -110, 88, 8, 84, -8, 60, -37, 60, 67, -43, -18, -127, 2, 1, 26, -74, 120, -124, 70, 11, + 20, -108, 64, -113, 104, -119, 88, -99, -24, 112, -105, -71, 112, 117, -44, 22, 88, -90, 32, 8, + -27, 48, 33, 76, 119, 78, -52, 89, 101, 20, -100, -12, 49, 21, 24, 84, 97, 12, 59, 19, + 46, -44, -36, 75, 65, 70, 70, 118, -83, -2, 67, 101, -21, 80, -123, -65, -69, -64, -79, -68, + 127, -48, -106, 4, -6, -113, -37, 111, 38, -57, 11, 112, 71, 102, 113, -50, 0, 0, 0, 0, + 73, 69, 78, 68, -82, 66, 96, -126}; + private static final byte[] UPDIR = { + -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42, + 0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71, + 68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0, + 0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -111, 73, 68, 65, 84, + 88, -123, -19, -106, -51, 74, -21, 64, 20, -57, -1, -87, -47, 7, -15, 41, 92, 118, -47, 82, + 10, -59, 110, 20, -95, 32, 77, -101, 44, -92, -37, -66, 80, -23, 3, 8, -126, -76, -48, -115, + -120, 43, 117, -19, -109, -44, -113, -50, -57, -103, 115, 87, 103, 76, 106, -117, 77, 19, -17, -107, + 75, -2, 48, -52, -112, 100, 38, -65, 57, -25, -52, 57, 3, 84, -86, 84, -87, -46, -1, -83, + 36, 73, 56, 73, 18, -2, -87, -11, -61, 50, 22, -119, -29, -104, 47, 47, 47, 97, -116, -127, + -75, -106, 39, -109, 73, 80, -58, -70, 105, 21, 94, 48, -114, 99, 78, -110, 4, 68, 4, -91, + 20, -116, 49, 120, 121, 121, -63, 120, 60, 46, 21, -74, 86, 100, -14, 112, 56, -28, -85, -85, + 43, 0, -128, -75, 22, -50, 57, 48, 51, -114, -113, -113, -47, -21, -11, 74, 13, -125, -67, 65, + -121, -61, 33, -113, 70, 35, 4, 65, 0, 99, -116, 111, -42, 90, 16, 17, 58, -99, 14, 46, + 46, 46, 74, -125, -35, 11, 84, 44, -23, -100, -61, -57, -57, 7, 86, -85, 21, -116, 49, 32, + 34, 111, 89, 107, 45, 90, -83, 22, -50, -50, -50, 74, -127, -51, 125, -104, 6, -125, 1, -57, + 113, -20, -83, -89, -108, -126, -75, 22, 0, -32, -100, -13, 45, 8, 2, 48, 51, -102, -51, 38, + -100, 115, 124, 125, 125, 93, 40, 102, 115, -127, 14, 6, 3, -114, -94, -56, -69, 87, 107, 13, + 34, 2, 0, -1, 44, 8, -78, 60, 68, -124, 70, -93, 1, 34, -30, -101, -101, -101, -67, 97, + 119, 6, -115, -94, -120, 123, -67, -98, -121, 19, 43, 10, -88, -12, -58, 24, 56, -25, -4, 55, + -78, -127, 122, -67, 14, 34, -30, -37, -37, -37, -67, 96, 119, -102, 20, 69, 17, 119, -69, 93, + 15, 98, -83, -59, -63, -63, 1, 0, 32, 12, -61, 12, -88, -124, -61, -37, -37, 27, -34, -33, + -33, -3, -122, 68, 15, 15, 15, -104, -49, -25, -71, 97, -65, -99, -48, -17, -9, -71, -35, 110, + -5, 83, 45, 58, 58, 58, -14, -112, 34, -79, -30, -21, -21, 43, -106, -53, 37, -76, -42, 0, + 0, -26, -49, -13, 20, -122, 33, -18, -17, -17, -79, 88, 44, 114, -63, 126, -21, 122, 102, -58, + 108, 54, -13, 22, 19, 32, 0, 56, 61, 61, -3, -14, 76, 107, -115, -43, 106, 5, 34, -62, + -45, -45, 83, -26, -35, -6, -72, 84, -48, -23, 116, -70, 113, -25, -25, -25, -25, -20, -100, -53, + -4, 60, 29, -109, 114, -6, 103, -77, 89, 41, 21, 106, -17, 90, 47, -112, 2, 8, -64, 3, + -118, -85, -45, -33, 20, -43, -34, -96, -52, -100, 57, -35, -64, 103, -116, -82, 3, -1, 83, 80, + -87, 62, 2, -72, -98, 79, 5, -74, 44, -107, 2, 42, 112, 0, 50, -15, -7, 43, 64, -103, + 121, 35, -24, -81, 118, -67, -28, -41, -76, -53, -91, -43, -21, 117, -106, 119, -23, 94, -58, -113, + -113, -113, 59, 101, -123, -62, -96, -101, -54, -87, 88, -14, -28, -28, 4, -121, -121, -121, 25, 48, + -71, 35, 40, -91, -16, -4, -4, -68, -13, -1, 10, -71, 62, 125, -75, 91, 7, -83, -43, 106, + 30, 74, 42, -104, -28, 89, -83, 53, -76, -42, -71, 98, -72, -112, 69, -91, 68, 110, -69, -96, + -92, 123, -7, 70, 41, 5, -91, 84, -18, 10, 85, 10, 104, 58, 70, -45, 125, -6, -80, 73, + 47, 22, -51, -85, 66, -82, 87, 74, 1, -128, -17, 55, -127, 109, 2, -1, -85, -96, 98, -47, + -76, 91, -73, 1, -82, -113, 1, -8, 107, -30, -113, -125, -34, -35, -35, 5, 68, -28, 19, -27, + -74, -12, 83, 102, -46, -81, 84, -87, 82, -91, 95, -88, 63, 49, -122, -88, 68, 127, -55, -90, + 73, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; + private static final byte[] ARROW = {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 48, + 0, 0, 0, 117, 8, 3, 0, 0, 0, 63, 73, -110, 106, 0, 0, 2, -9, 80, 76, 84, + 69, -46, -46, -46, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -65, -65, -65, 0, + 0, 0, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -54, -54, -54, + -48, -48, -48, -48, -48, -48, -47, -47, -47, -47, -47, -47, -49, -49, -49, -86, -86, -86, -48, -48, + -48, -48, -48, -48, -47, -47, -47, -52, -52, -52, -49, -49, -49, -48, -48, -48, -48, -48, -48, -1, + -1, -1, -49, -49, -49, -48, -48, -48, -48, -48, -48, -51, -51, -51, -50, -50, -50, -48, -48, -48, + -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52, -52, -52, -48, -48, + -48, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, -52, -52, -52, -48, -48, -48, -48, + -48, -48, -1, -1, -1, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, + -48, -48, -48, -41, -41, -41, -48, -48, -48, -49, -49, -49, -43, -43, -43, -47, -47, -47, -49, -49, + -49, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49, -47, + -47, -47, -49, -49, -49, -47, -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49, + -47, -47, -47, -44, -44, -44, -48, -48, -48, -48, -48, -48, -1, -1, -1, -46, -46, -46, -48, -48, + -48, -49, -49, -49, -47, -47, -47, -49, -49, -49, -35, -35, -35, -49, -49, -49, -49, -49, -49, -49, + -49, -49, -47, -47, -47, -40, -40, -40, -49, -49, -49, -48, -48, -48, -48, -48, -48, -47, -47, -47, + -47, -47, -47, -47, -47, -47, -48, -48, -48, -60, -60, -60, -53, -53, -53, -48, -48, -48, -46, -46, + -46, -65, -65, -65, -48, -48, -48, -54, -54, -54, -45, -45, -45, -47, -47, -47, -49, -49, -49, -48, + -48, -48, -49, -49, -49, -51, -51, -51, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, + -47, -47, -47, -37, -37, -37, -47, -47, -47, -49, -49, -49, -50, -50, -50, -48, -48, -48, -128, -128, + -128, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47, + -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47, -47, -47, -48, -48, -48, + -50, -50, -50, -49, -49, -49, -48, -48, -48, -48, -48, -48, -33, -33, -33, -48, -48, -48, -52, -52, + -52, -51, -51, -51, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52, + -52, -52, -48, -48, -48, -46, -46, -46, -48, -48, -48, -37, -37, -37, -29, -29, -29, -48, -48, -48, + -48, -48, -48, -47, -47, -47, -48, -48, -48, -49, -49, -49, -46, -46, -46, -48, -48, -48, -49, -49, + -49, -47, -47, -47, -58, -58, -58, -49, -49, -49, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47, + -47, -47, -50, -50, -50, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, + -48, -48, -48, -55, -55, -55, -45, -45, -45, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, + -48, -46, -46, -46, -47, -47, -47, -48, -48, -48, -47, -47, -47, -51, -51, -51, -47, -47, -47, -51, + -51, -51, -48, -48, -48, -47, -47, -47, -48, -48, -48, -65, -65, -65, -48, -48, -48, -50, -50, -50, + -48, -48, -48, -47, -47, -47, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -48, -48, + -48, -50, -50, -50, -47, -47, -47, -49, -49, -49, -49, -49, -49, -47, -47, -47, -48, -48, -48, -47, + -47, -47, -49, -49, -49, -39, -39, -39, -50, -50, -50, -47, -47, -47, -48, -48, -48, -42, -42, -42, + -47, -47, -47, -46, -46, -46, -47, -47, -47, -47, -47, -47, -48, -48, -48, -49, -49, -49, -49, -49, + -49, -47, -47, -47, -48, -48, -48, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47, -47, -47, -48, + -48, -48, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, -50, -50, -50, -47, -47, -47, + -56, -56, -56, -48, -48, -48, -48, -48, -48, -40, -40, -40, -45, -45, -45, -47, -47, -47, -47, -47, + -47, -49, -49, -49, -48, -48, -48, -43, -43, -43, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47, + -47, -47, -50, -50, -50, -49, -49, -49, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, + -115, 52, -27, 40, 0, 0, 0, -3, 116, 82, 78, 83, 34, -37, -1, -7, 120, 4, 0, 5, + 118, -9, -41, 38, 24, -46, -2, -100, 78, 69, 3, -98, -54, 28, 40, -37, -8, 115, 3, 122, + -6, -43, 36, 31, -49, -109, 78, 70, -93, -50, 20, 43, -38, -10, 113, 123, 30, -45, -115, 2, + 86, -5, -9, 65, -94, -55, 19, -29, 111, 6, 127, 32, -39, -3, -114, 92, -4, 58, -86, -63, + 22, -32, 109, -126, -5, -51, 36, -44, -121, 1, 90, -11, 59, -79, -59, 15, 48, -11, 106, -123, + 26, 37, -40, 98, -3, -14, -80, -61, 13, 44, -25, 103, 8, -120, 29, 35, -35, -4, -127, 101, + 46, -76, -71, 17, -24, 99, 7, -118, -58, 42, 124, 2, 103, -13, 47, -67, -65, 12, 55, -27, + -116, -57, 24, 44, -35, 119, 112, -21, 49, 8, -70, 10, 51, -19, -14, 93, -111, -62, 25, -30, + 119, -78, 14, 9, -108, 113, 117, -18, -53, 63, -23, -15, 89, 9, -106, -2, -66, 21, 50, -125, + -33, -28, 57, 97, -97, -73, 19, 69, -12, -17, 81, -87, 57, 94, -99, -73, 72, -18, 82, -88, + -20, 27, 12, -100, 67, -16, 83, 16, -91, -122, 54, -26, 104, -101, 64, 85, 11, -82, 100, -101, + 20, 84, -47, 125, 31, -102, 62, -57, -88, -125, 107, -102, -69, -15, -53, -77, 52, -31, -105, 66, + -21, 87, 121, 110, -67, 23, -60, -84, 13, 46, 111, 88, -75, 119, 42, 50, -32, 106, -107, 63, + 91, -78, 117, 114, -108, 41, -51, -28, -65, 0, 0, 3, -107, 73, 68, 65, 84, 88, -61, -107, + -40, 105, 84, -108, 85, 24, 7, 112, -4, -113, -13, -112, -37, -101, -111, -96, 78, 67, -94, 38, + -18, -66, 14, 46, 52, -38, -28, -96, 34, 90, -67, -31, -106, 75, -114, 70, 74, 42, 90, 86, + -109, 4, 38, -88, -111, -90, 50, -126, -72, -46, -54, -102, 38, -102, -53, 104, -101, 123, -88, -71, + 78, -18, -91, -90, 88, 82, 46, 89, 90, -71, -107, -27, 7, 95, 62, 120, -114, -100, -93, -121, + -5, 127, 63, -65, -65, 15, -9, -36, 123, -97, -5, 127, -98, -96, -96, 106, -80, 84, -73, -118, + -14, 23, 20, -4, 64, 13, -44, -84, 69, 0, -87, 93, 7, -38, -125, 12, -112, -70, 22, -32, + 33, 6, -124, 60, 12, -44, 11, 37, -128, -124, -43, 71, -125, -122, 33, 4, -80, 61, 98, 71, + -8, -93, 4, -112, 70, 17, -48, 26, 51, 64, -102, 0, 53, -102, 50, 64, 30, -45, -48, 44, + -110, 1, -51, 91, -96, 101, 43, 43, 1, -84, -83, -19, -88, -39, -122, 0, 18, -38, 22, 104, + -89, 19, 64, -38, 59, 16, -43, -127, 1, -42, -114, 64, -89, -50, 4, -112, -80, 104, 68, 61, + -82, 19, -64, -39, -59, -126, -120, -82, 4, -112, -48, 39, -32, 122, -110, 1, -46, -51, 1, 119, + 12, 3, -84, -35, -127, 30, 61, 9, 32, -51, 99, 17, -43, 75, 39, 64, 112, 92, 111, -12, + 105, 68, 0, 121, -22, 105, 104, -49, 48, -64, 120, 22, -120, -17, 75, 0, -111, 126, 64, -1, + 1, 12, 24, 24, -117, -25, 6, -39, 8, -32, 28, -20, 64, -77, 33, 4, -112, -95, -49, 3, + 113, 6, 1, 100, -104, 7, -61, 99, 24, -32, 28, 1, -68, -112, 64, 0, 121, 113, 36, -30, + 71, -23, 4, 72, 124, -55, -114, 78, -93, 9, 32, 99, -58, 34, 105, 28, 3, 100, -68, 27, + 13, 94, 102, -128, -13, 21, 96, -62, -85, 4, -112, -127, -81, 33, -2, 117, -125, 0, -34, 55, + -110, 48, 49, -108, 0, -110, -36, 2, -82, 55, 25, -112, -110, 10, 76, 122, -117, 0, 34, -109, + -127, -76, 116, 6, 76, -103, -118, 73, -61, 116, 2, 120, -89, -71, -15, 118, 6, 1, -28, -99, + -23, -64, 12, -125, 0, -58, -69, -64, -52, 89, 4, -112, -39, -26, -70, 51, 125, 4, -112, 57, + 89, -56, -98, -101, 66, 0, 95, -114, 3, -13, -26, 19, 64, -110, 23, -64, -79, -112, 1, 82, + -35, -123, -8, -95, 12, -16, -103, -21, 94, -76, -104, 0, 50, 101, 38, -122, 119, 51, 8, -112, + -5, -98, 11, 105, -75, 9, 80, 81, 58, -19, -17, 51, 64, -1, 0, -8, 112, 12, 1, -60, + -6, -111, -71, -18, -39, 4, -112, -113, -13, -112, 95, -112, 66, -128, -62, 34, -83, -8, -109, 37, + 4, -112, -91, -79, -16, -92, 50, -64, -8, 20, 88, -58, 0, -55, 1, 74, 24, -80, 124, 36, + 44, 43, 8, -112, -16, -103, -85, 120, -27, 42, 2, -84, 94, 3, -1, 90, 67, 29, -92, -41, + 1, -42, 89, -43, 55, -50, 54, 24, -120, 14, 34, -114, -58, -25, 37, 112, 127, 65, 28, 62, + -33, -105, -59, -38, 87, -99, 9, -16, -11, 122, -8, 55, 16, 23, -56, 23, 14, 108, -12, -86, + 95, 81, 91, 28, -80, 105, 51, 81, 4, -52, 10, 110, 73, 37, -54, -52, -106, -83, 46, -84, + -116, 36, -64, 55, 107, 80, -70, -115, -88, -83, -37, -21, 1, 59, -104, 98, -4, -83, -122, -84, + -47, 68, -71, -33, -71, 11, -10, -35, -60, 11, -108, -66, -57, -115, -52, 72, 2, -20, -51, -122, + 127, -97, -95, 14, 2, 17, -64, 119, 78, -11, 103, 87, -17, 98, -34, -29, 90, -60, -61, -66, + 60, 26, -98, -3, 68, 116, -16, 29, -48, -76, -52, -125, 4, 88, -67, 9, -2, 67, 68, -4, + -15, 30, 6, -70, 39, -86, 7, 44, -37, 17, 32, -21, 40, 17, -31, 118, -106, -64, 50, -120, + 8, -119, -127, -17, 93, -104, -80, -124, 0, 63, 100, 35, -65, 61, -111, -116, -73, 31, 3, -114, + 7, 8, 112, -62, 60, -43, 109, -120, -80, -66, 116, 42, -20, 63, 18, -3, 67, -32, -92, 27, + -89, 14, 18, -96, 111, -39, -67, 79, -11, -3, -128, -17, 52, -16, -109, 83, -67, 105, -46, -51, + 61, -34, 21, 70, -76, 101, 63, -97, -127, 103, 60, -47, -8, 21, -106, 87, 122, -110, -85, 6, + -65, -4, -118, -46, -77, -122, 58, -16, -98, -82, 28, 43, -86, 2, 33, -25, -52, -32, 114, -108, + 104, -64, -25, -100, -127, -3, 60, -47, -30, -5, -54, 93, -72, -16, 27, 1, 98, -4, -56, 46, + 32, -26, 26, 9, 23, -127, -33, -1, 32, -64, 17, 13, 101, 42, 73, -27, -50, 63, -105, 74, + -48, -5, 50, 49, -3, 9, 20, 37, -95, -86, 61, -82, 4, 98, 74, -111, -1, -89, -95, 14, + 114, -51, 123, -4, 87, -94, -6, -56, -53, -8, 27, -56, -69, 66, 12, -43, -82, 94, -125, -25, + 58, 49, -74, 43, -68, -95, 21, 87, 57, -109, -71, 27, -4, -109, -121, -78, 127, 83, -44, -127, + -43, 124, -113, 111, 22, -118, 50, -48, -1, 3, -4, -86, -109, -54, 10, 80, 17, -8, -1, 23, + 117, -112, 123, -53, 108, 41, 50, -44, -63, 109, -80, -19, 79, -78, 17, 39, 44, -102, 0, 0, + 0, 0, 73, 69, 78, 68, -82, 66, 96, -126}; + +} diff --git a/app/src/main/java/io/legado/app/ui/font/FontAdapter.kt b/app/src/main/java/io/legado/app/ui/font/FontAdapter.kt new file mode 100755 index 000000000..7cb8a4a9b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/font/FontAdapter.kt @@ -0,0 +1,72 @@ +package io.legado.app.ui.font + +import android.content.Context +import android.graphics.Typeface +import android.os.Build +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.databinding.ItemFontBinding +import io.legado.app.utils.* +import java.io.File +import java.net.URLDecoder + +class FontAdapter(context: Context, curFilePath: String, val callBack: CallBack) : + RecyclerAdapter(context) { + + private val curName = kotlin.runCatching { + URLDecoder.decode(curFilePath, "utf-8") + }.getOrNull()?.substringAfterLast(File.separator) + + override fun getViewBinding(parent: ViewGroup): ItemFontBinding { + return ItemFontBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFontBinding, + item: FileDoc, + payloads: MutableList + ) { + binding.run { + kotlin.runCatching { + val typeface: Typeface? = if (item.isContentScheme) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.contentResolver + .openFileDescriptor(item.uri, "r")?.use { + Typeface.Builder(it.fileDescriptor).build() + } + } else { + Typeface.createFromFile(RealPathUtil.getPath(context, item.uri)) + } + } else { + Typeface.createFromFile(item.uri.path!!) + } + tvFont.typeface = typeface + }.onFailure { + it.printOnDebug() + AppLog.put("读取字体 ${item.name} 出错\n${it.localizedMessage}", it, true) + } + tvFont.text = item.name + root.setOnClickListener { callBack.onFontSelect(item) } + if (item.name == curName) { + ivChecked.visible() + } else { + ivChecked.invisible() + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFontBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.onFontSelect(it) + } + } + } + + interface CallBack { + fun onFontSelect(docItem: FileDoc) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt new file mode 100755 index 000000000..c50993a17 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt @@ -0,0 +1,209 @@ +package io.legado.app.ui.font + +import android.net.Uri +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.appcompat.widget.Toolbar +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.DialogFontSelectBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.RealPathUtil +import io.legado.app.utils.applyTint +import io.legado.app.utils.cnCompare +import io.legado.app.utils.externalFiles +import io.legado.app.utils.getPrefString +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.list +import io.legado.app.utils.listFileDocs +import io.legado.app.utils.putPrefString +import io.legado.app.utils.setLayout +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import java.io.File + +/** + * 字体选择对话框 + */ +class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select), + Toolbar.OnMenuItemClickListener, + FontAdapter.CallBack { + private val fontRegex = Regex("(?i).*\\.[ot]tf") + private val binding by viewBinding(DialogFontSelectBinding::bind) + private val adapter by lazy { + val curFontPath = callBack?.curFontPath ?: "" + FontAdapter(requireContext(), curFontPath, this) + } + private val selectFontDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + if (uri.isContentScheme()) { + putPrefString(PreferKey.fontFolder, uri.toString()) + val doc = DocumentFile.fromTreeUri(requireContext(), uri) + if (doc != null) { + loadFontFiles(FileDoc.fromDocumentFile(doc)) + } else { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + loadFontFilesByPermission(path) + } + } + } else { + uri.path?.let { path -> + putPrefString(PreferKey.fontFolder, path) + loadFontFilesByPermission(path) + } + } + } + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.select_font) + binding.toolBar.inflateMenu(R.menu.font_select) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.adapter = adapter + + val fontPath = getPrefString(PreferKey.fontFolder) + if (fontPath.isNullOrEmpty()) { + openFolder() + } else { + if (fontPath.isContentScheme()) { + val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath)) + if (doc?.canRead() == true) { + loadFontFiles(FileDoc.fromDocumentFile(doc)) + } else { + openFolder() + } + } else { + loadFontFilesByPermission(fontPath) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_default -> { + val requireContext = requireContext() + alert(titleResource = R.string.system_typeface) { + items( + requireContext.resources.getStringArray(R.array.system_typefaces).toList() + ) { _, i -> + AppConfig.systemTypefaces = i + onDefaultFontChange() + dismissAllowingStateLoss() + } + } + } + R.id.menu_other -> { + openFolder() + } + } + return true + } + + private fun openFolder() { + lifecycleScope.launch { + val defaultPath = "SD${File.separator}Fonts" + selectFontDir.launch { + otherActions = arrayListOf(SelectItem(defaultPath, -1)) + } + } + } + + private fun getLocalFonts(): ArrayList { + val path = FileUtils.getPath(requireContext().externalFiles, "font") + return File(path).listFileDocs { + it.name.matches(fontRegex) + } + } + + private fun loadFontFilesByPermission(path: String) { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + loadFontFiles( + FileDoc.fromFile(File(path)) + ) + } + .request() + } + + private fun loadFontFiles(fileDoc: FileDoc) { + execute { + val fontItems = fileDoc.list { + it.name.matches(fontRegex) + } ?: ArrayList() + mergeFontItems(fontItems, getLocalFonts()) + }.onSuccess { + adapter.setItems(it) + }.onError { + AppLog.put("加载字体文件失败\n${it.localizedMessage}", it) + toastOnUi("getFontFiles:${it.localizedMessage}") + } + } + + private fun mergeFontItems( + items1: ArrayList, + items2: ArrayList + ): List { + val items = ArrayList(items1) + items2.forEach { item2 -> + var isInFirst = false + items1.forEach for1@{ item1 -> + if (item2.name == item1.name) { + isInFirst = true + return@for1 + } + } + if (!isInFirst) { + items.add(item2) + } + } + return items.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + } + + override fun onFontSelect(docItem: FileDoc) { + execute { + callBack?.selectFont(docItem.toString()) + }.onSuccess { + dismissAllowingStateLoss() + } + } + + private fun onDefaultFontChange() { + callBack?.selectFont("") + } + + private val callBack: CallBack? + get() = (parentFragment as? CallBack) ?: (activity as? CallBack) + + interface CallBack { + fun selectFont(path: String) + val curFontPath: String + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt b/app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt new file mode 100755 index 000000000..464ea27b2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/login/SourceLoginActivity.kt @@ -0,0 +1,36 @@ +package io.legado.app.ui.login + +import android.os.Bundle +import androidx.activity.viewModels +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.data.entities.BaseSource +import io.legado.app.databinding.ActivitySourceLoginBinding +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.viewbindingdelegate.viewBinding + + +class SourceLoginActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivitySourceLoginBinding::inflate) + override val viewModel by viewModels() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.initData(intent, success = { source -> + initView(source) + }, error = { + finish() + }) + } + + private fun initView(source: BaseSource) { + if (source.loginUi.isNullOrEmpty()) { + supportFragmentManager.beginTransaction() + .replace(R.id.fl_fragment, WebViewLoginFragment(), "webViewLogin") + .commit() + } else { + showDialogFragment() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt b/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt new file mode 100755 index 000000000..5b89b7144 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt @@ -0,0 +1,198 @@ +package io.legado.app.ui.login + +import android.content.DialogInterface +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import androidx.core.view.setPadding +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.script.rhino.runScriptWithContext +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.rule.RowUi +import io.legado.app.databinding.DialogLoginBinding +import io.legado.app.databinding.ItemFilletTextBinding +import io.legado.app.databinding.ItemSourceEditBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.utils.GSON +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.openUrl +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setLayout +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import splitties.views.onClick + + +class SourceLoginDialog : BaseDialogFragment(R.layout.dialog_login, true) { + + private val binding by viewBinding(DialogLoginBinding::bind) + private val viewModel by activityViewModels() + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val source = viewModel.source ?: return + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.title = getString(R.string.login_source, source.getTag()) + val loginInfo = source.getLoginInfoMap() + val loginUi = source.loginUi() + try { + loginUi?.forEachIndexed { index, rowUi -> + when (rowUi.type) { + RowUi.Type.text -> ItemSourceEditBinding.inflate( + layoutInflater, + binding.root, + false + ).let { + binding.flexbox.addView(it.root) + it.root.id = index + 1000 + it.textInputLayout.hint = rowUi.name + it.editText.setText(loginInfo?.get(rowUi.name)) + } + + RowUi.Type.password -> ItemSourceEditBinding.inflate( + layoutInflater, + binding.root, + false + ).let { + binding.flexbox.addView(it.root) + it.root.id = index + 1000 + it.textInputLayout.hint = rowUi.name + it.editText.inputType = + InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT + it.editText.setText(loginInfo?.get(rowUi.name)) + } + + RowUi.Type.button -> ItemFilletTextBinding.inflate( + layoutInflater, + binding.root, + false + ).let { + binding.flexbox.addView(it.root) + rowUi.style().apply(it.root) + it.root.id = index + 1000 + it.textView.text = rowUi.name + it.textView.setPadding(16.dpToPx()) + it.root.onClick { + handleButtonClick(source, rowUi, loginUi) + } + } + } + } + } catch (e: NullPointerException) { + AppLog.put("登录UI JSON 数据错误", e, true) + } + binding.toolBar.inflateMenu(R.menu.source_login) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menu_ok -> { + val loginData = getLoginData(loginUi) + login(source, loginData) + } + + R.id.menu_show_login_header -> alert { + setTitle(R.string.login_header) + source.getLoginHeader()?.let { loginHeader -> + setMessage(loginHeader) + positiveButton(R.string.copy_text) { + appCtx.sendToClip(loginHeader) + } + } + } + + R.id.menu_del_login_header -> source.removeLoginHeader() + R.id.menu_log -> showDialogFragment() + } + return@setOnMenuItemClickListener true + } + } + + private fun handleButtonClick(source: BaseSource, rowUi: RowUi, loginUi: List) { + lifecycleScope.launch(IO) { + if (rowUi.action.isAbsUrl()) { + context?.openUrl(rowUi.action!!) + } else if (rowUi.action != null) { + // JavaScript + val buttonFunctionJS = rowUi.action!! + val loginJS = source.getLoginJs() ?: return@launch + kotlin.runCatching { + runScriptWithContext { + source.evalJS("$loginJS\n$buttonFunctionJS") { + put("result", getLoginData(loginUi)) + } + } + }.onFailure { e -> + ensureActive() + AppLog.put("LoginUI Button ${rowUi.name} JavaScript error", e) + } + } + } + } + + private fun getLoginData(loginUi: List?): HashMap { + val loginData = hashMapOf() + loginUi?.forEachIndexed { index, rowUi -> + when (rowUi.type) { + "text", "password" -> { + val rowView = binding.root.findViewById(index + 1000) + ItemSourceEditBinding.bind(rowView).editText.text?.let { + loginData[rowUi.name] = it.toString() + } + } + } + } + return loginData + } + + private fun login(source: BaseSource, loginData: HashMap) { + lifecycleScope.launch(IO) { + if (loginData.isEmpty()) { + source.removeLoginInfo() + withContext(Main) { + dismiss() + } + } else if (source.putLoginInfo(GSON.toJson(loginData))) { + try { + runScriptWithContext { + source.login() + } + context?.toastOnUi(R.string.success) + withContext(Main) { + dismiss() + } + } catch (e: Exception) { + AppLog.put("登录出错\n${e.localizedMessage}", e) + context?.toastOnUi("登录出错\n${e.localizedMessage}") + e.printOnDebug() + } + } + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + activity?.finish() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLoginViewModel.kt b/app/src/main/java/io/legado/app/ui/login/SourceLoginViewModel.kt new file mode 100755 index 000000000..73742fd47 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/login/SourceLoginViewModel.kt @@ -0,0 +1,43 @@ +package io.legado.app.ui.login + +import android.app.Application +import android.content.Intent +import com.script.rhino.runScriptWithContext +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.BaseSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.utils.toastOnUi + +class SourceLoginViewModel(application: Application) : BaseViewModel(application) { + + var source: BaseSource? = null + var headerMap: Map = emptyMap() + + fun initData(intent: Intent, success: (bookSource: BaseSource) -> Unit, error: () -> Unit) { + execute { + val sourceKey = intent.getStringExtra("key") + ?: throw NoStackTraceException("没有参数") + when (intent.getStringExtra("type")) { + "bookSource" -> source = appDb.bookSourceDao.getBookSource(sourceKey) + "rssSource" -> source = appDb.rssSourceDao.getByKey(sourceKey) + "httpTts" -> source = appDb.httpTTSDao.get(sourceKey.toLong()) + } + headerMap = runScriptWithContext { + source?.getHeaderMap(true) ?: emptyMap() + } + source + }.onSuccess { + if (it != null) { + success.invoke(it) + } else { + context.toastOnUi("未找到书源") + } + }.onError { + error.invoke() + AppLog.put("登录 UI 初始化失败\n$it", it, true) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/login/WebViewLoginFragment.kt b/app/src/main/java/io/legado/app/ui/login/WebViewLoginFragment.kt new file mode 100755 index 000000000..70ebdd227 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/login/WebViewLoginFragment.kt @@ -0,0 +1,157 @@ +package io.legado.app.ui.login + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.net.Uri +import android.net.http.SslError +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.webkit.CookieManager +import android.webkit.SslErrorHandler +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.activityViewModels +import io.legado.app.R +import io.legado.app.base.BaseFragment +import io.legado.app.constant.AppConst +import io.legado.app.data.entities.BaseSource +import io.legado.app.databinding.FragmentWebViewLoginBinding +import io.legado.app.help.http.CookieStore +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.gone +import io.legado.app.utils.longSnackbar +import io.legado.app.utils.openUrl +import io.legado.app.utils.snackbar +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class WebViewLoginFragment : BaseFragment(R.layout.fragment_web_view_login) { + + private val binding by viewBinding(FragmentWebViewLoginBinding::bind) + private val viewModel by activityViewModels() + + private var checking = false + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + viewModel.source?.let { + binding.titleBar.title = getString(R.string.login_source, it.getTag()) + initWebView(it) + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.source_webview_login, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + when (item.itemId) { + R.id.menu_ok -> { + if (!checking) { + checking = true + binding.titleBar.snackbar(R.string.check_host_cookie) + viewModel.source?.let { + loadUrl(it) + } + } + } + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun initWebView(source: BaseSource) { + binding.progressBar.fontColor = accentColor + binding.webView.settings.apply { + mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + domStorageEnabled = true + useWideViewPort = true + loadWithOverviewMode = true + builtInZoomControls = true + javaScriptEnabled = true + displayZoomControls = false + viewModel.headerMap[AppConst.UA_NAME]?.let { + userAgentString = it + } + } + val cookieManager = CookieManager.getInstance() + binding.webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + val cookie = cookieManager.getCookie(url) + CookieStore.setCookie(source.getKey(), cookie) + super.onPageStarted(view, url, favicon) + } + + override fun onPageFinished(view: WebView?, url: String?) { + val cookie = cookieManager.getCookie(url) + CookieStore.setCookie(source.getKey(), cookie) + if (checking) { + activity?.finish() + } + super.onPageFinished(view, url) + } + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + return shouldOverrideUrlLoading(request.url) + } + + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "KotlinRedundantDiagnosticSuppress") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return shouldOverrideUrlLoading(Uri.parse(url)) + } + + private fun shouldOverrideUrlLoading(url: Uri): Boolean { + when (url.scheme) { + "http", "https" -> { + return false + } + + else -> { + binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) { + context?.openUrl(url) + } + return true + } + } + } + + @SuppressLint("WebViewClientOnReceivedSslError") + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + handler?.proceed() + } + } + binding.webView.webChromeClient = object : WebChromeClient() { + + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + binding.progressBar.setDurProgress(newProgress) + binding.progressBar.gone(newProgress == 100) + } + + } + loadUrl(source) + } + + private fun loadUrl(source: BaseSource) { + val loginUrl = source.loginUrl ?: return + val absoluteUrl = NetworkUtils.getAbsoluteURL(source.getKey(), loginUrl) + binding.webView.loadUrl(absoluteUrl, viewModel.headerMap) + } + + override fun onDestroy() { + super.onDestroy() + binding.webView.destroy() + } + +} diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt new file mode 100755 index 000000000..44aade583 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -0,0 +1,466 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.ui.main + +import android.os.Bundle +import android.text.format.DateUtils +import android.view.MenuItem +import android.view.ViewGroup +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.core.view.get +import androidx.core.view.postDelayed +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.lifecycleScope +import androidx.viewpager.widget.ViewPager +import com.google.android.material.bottomnavigation.BottomNavigationView +import io.legado.app.BuildConfig +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst.appInfo +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.ActivityMainBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.LocalConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.storage.Backup +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.elevation +import io.legado.app.lib.theme.primaryColor +import io.legado.app.service.BaseReadAloudService +import io.legado.app.ui.about.CrashLogsDialog +import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment +import io.legado.app.ui.main.bookshelf.style1.BookshelfFragment1 +import io.legado.app.ui.main.bookshelf.style2.BookshelfFragment2 +import io.legado.app.ui.main.explore.ExploreFragment +import io.legado.app.ui.main.my.MyFragment +import io.legado.app.ui.main.rss.RssFragment +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.ui.widget.text.BadgeView +import io.legado.app.utils.isCreated +import io.legado.app.utils.navigationBarHeight +import io.legado.app.utils.observeEvent +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import splitties.views.bottomPadding +import kotlin.coroutines.resume + +/** + * 主界面 + */ +@Suppress("PrivatePropertyName") +class MainActivity : VMBaseActivity(), + BottomNavigationView.OnNavigationItemSelectedListener, + BottomNavigationView.OnNavigationItemReselectedListener { + + override val binding by viewBinding(ActivityMainBinding::inflate) + override val viewModel by viewModels() + private val idBookshelf = 0 + private val idBookshelf1 = 11 + private val idBookshelf2 = 12 + private val idExplore = 1 + private val idRss = 2 + private val idMy = 3 + private var exitTime: Long = 0 + private var bookshelfReselected: Long = 0 + private var exploreReselected: Long = 0 + private var pagePosition = 0 + private val fragmentMap = hashMapOf() + private var bottomMenuCount = 4 + private val EXIT_INTERVAL = 2000L + private val realPositions = arrayOf(idBookshelf, idExplore, idRss, idMy) + private val adapter by lazy { + TabFragmentPageAdapter(supportFragmentManager) + } + private var onUpBooksBadgeView: BadgeView? = null + + override fun onActivityCreated(savedInstanceState: Bundle?) { + upBottomMenu() + initView() + upHomePage() + onBackPressedDispatcher.addCallback(this) { + if (pagePosition != 0) { + binding.viewPagerMain.currentItem = 0 + return@addCallback + } + (fragmentMap[getFragmentId(0)] as? BookshelfFragment2)?.let { + if (it.back()) { + return@addCallback + } + } + if (System.currentTimeMillis() - exitTime > EXIT_INTERVAL) { + toastOnUi(R.string.double_click_exit) + exitTime = System.currentTimeMillis() + } else { + if (BaseReadAloudService.pause) { + finish() + } else { + moveTaskToBack(true) + } + } + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + lifecycleScope.launch { + //隐私协议 + if (!privacyPolicy()) return@launch + //版本更新 + upVersion() + //设置本地密码 + setLocalPassword() + notifyAppCrash() + //备份同步 + backupSync() + //自动更新书籍 + val isAutoRefreshedBook = savedInstanceState?.getBoolean("isAutoRefreshedBook") ?: false + if (AppConfig.autoRefreshBook && !isAutoRefreshedBook) { + binding.viewPagerMain.postDelayed(1000) { + viewModel.upAllBookToc() + } + } + binding.viewPagerMain.postDelayed(3000) { + viewModel.postLoad() + } + } + } + + override fun onNavigationItemSelected(item: MenuItem): Boolean = binding.run { + when (item.itemId) { + R.id.menu_bookshelf -> + viewPagerMain.setCurrentItem(0, false) + + R.id.menu_discovery -> + viewPagerMain.setCurrentItem(realPositions.indexOf(idExplore), false) + + R.id.menu_rss -> + viewPagerMain.setCurrentItem(realPositions.indexOf(idRss), false) + + R.id.menu_my_config -> + viewPagerMain.setCurrentItem(realPositions.indexOf(idMy), false) + } + return false + } + + override fun onNavigationItemReselected(item: MenuItem) { + when (item.itemId) { + R.id.menu_bookshelf -> { + if (System.currentTimeMillis() - bookshelfReselected > 300) { + bookshelfReselected = System.currentTimeMillis() + } else { + (fragmentMap[getFragmentId(0)] as? BaseBookshelfFragment)?.gotoTop() + } + } + + R.id.menu_discovery -> { + if (System.currentTimeMillis() - exploreReselected > 300) { + exploreReselected = System.currentTimeMillis() + } else { + (fragmentMap[1] as? ExploreFragment)?.compressExplore() + } + } + } + } + + private fun initView() = binding.run { + viewPagerMain.setEdgeEffectColor(primaryColor) + viewPagerMain.offscreenPageLimit = 3 + viewPagerMain.adapter = adapter + viewPagerMain.addOnPageChangeListener(PageChangeCallback()) + bottomNavigationView.elevation = elevation + bottomNavigationView.setOnNavigationItemSelectedListener(this@MainActivity) + bottomNavigationView.setOnNavigationItemReselectedListener(this@MainActivity) + if (AppConfig.isEInkMode) { + bottomNavigationView.setBackgroundResource(R.drawable.bg_eink_border_top) + } + bottomNavigationView.setOnApplyWindowInsetsListenerCompat { view, windowInsets -> + val height = windowInsets.navigationBarHeight + view.bottomPadding = height + windowInsets.inset(0, 0, 0, height) + } + } + + /** + * 用户隐私与协议 + */ + private suspend fun privacyPolicy(): Boolean = suspendCancellableCoroutine sc@{ block -> + if (LocalConfig.privacyPolicyOk) { + block.resume(true) + return@sc + } + val privacyPolicy = String(assets.open("privacyPolicy.md").readBytes()) + alert(getString(R.string.privacy_policy), privacyPolicy) { + positiveButton(R.string.agree) { + LocalConfig.privacyPolicyOk = true + block.resume(true) + } + negativeButton(R.string.refuse) { + finish() + block.resume(false) + } + } + } + + /** + * 版本更新日志 + */ + private suspend fun upVersion() = suspendCancellableCoroutine sc@{ block -> + if (LocalConfig.versionCode == appInfo.versionCode) { + block.resume(null) + return@sc + } + LocalConfig.versionCode = appInfo.versionCode + if (LocalConfig.isFirstOpenApp) { + val help = String(assets.open("web/help/md/appHelp.md").readBytes()) + val dialog = TextDialog(getString(R.string.help), help, TextDialog.Mode.MD) + dialog.setOnDismissListener { + block.resume(null) + } + showDialogFragment(dialog) + } else if (!BuildConfig.DEBUG) { + val log = String(assets.open("updateLog.md").readBytes()) + val dialog = TextDialog(getString(R.string.update_log), log, TextDialog.Mode.MD) + dialog.setOnDismissListener { + block.resume(null) + } + showDialogFragment(dialog) + } else { + block.resume(null) + } + } + + /** + * 设置本地密码 + */ + private suspend fun setLocalPassword() = suspendCancellableCoroutine sc@{ block -> + if (LocalConfig.password != null) { + block.resume(null) + return@sc + } + alert(R.string.set_local_password, R.string.set_local_password_summary) { + val editTextBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "password" + } + customView { + editTextBinding.root + } + onDismiss { + block.resume(null) + } + okButton { + LocalConfig.password = editTextBinding.editView.text.toString() + } + cancelButton { + LocalConfig.password = "" + } + } + } + + private fun notifyAppCrash() { + if (!LocalConfig.appCrash || BuildConfig.DEBUG) { + return + } + LocalConfig.appCrash = false + alert(getString(R.string.draw), "检测到阅读发生了崩溃,是否打开崩溃日志以便报告问题?") { + yesButton { + showDialogFragment() + } + noButton() + } + } + + /** + * 备份同步 + */ + private fun backupSync() { + if (!AppConfig.autoCheckNewBackup) { + return + } + lifecycleScope.launch { + val lastBackupFile = + withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch + if (lastBackupFile.lastModify - LocalConfig.lastBackup > DateUtils.MINUTE_IN_MILLIS) { + LocalConfig.lastBackup = lastBackupFile.lastModify + alert(R.string.restore, R.string.webdav_after_local_restore_confirm) { + cancelButton() + okButton { + viewModel.restoreWebDav(lastBackupFile.displayName) + } + } + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + if (AppConfig.autoRefreshBook) { + outState.putBoolean("isAutoRefreshedBook", true) + } + } + + override fun onDestroy() { + super.onDestroy() + Coroutine.async { + BookHelp.clearInvalidCache() + } + if (!BuildConfig.DEBUG) { + Backup.autoBack(this) + } + } + + /** + * 如果重启太快fragment不会重建,这里更新一下书架的排序 + */ + override fun recreate() { + (fragmentMap[getFragmentId(0)] as? BaseBookshelfFragment)?.run { + upSort() + } + super.recreate() + } + + override fun observeLiveBus() { + viewModel.onUpBooksLiveData.observe(this) { + if (onUpBooksBadgeView == null) { + onUpBooksBadgeView = binding.bottomNavigationView.addBadgeView(0) + } + onUpBooksBadgeView!!.setBadgeCount(it) + } + observeEvent(EventBus.RECREATE) { + recreate() + } + observeEvent(EventBus.NOTIFY_MAIN) { + binding.apply { + if (it) { + bottomNavigationView.menu.clear() + bottomNavigationView.inflateMenu(R.menu.main_bnv) + onUpBooksBadgeView = null + } + upBottomMenu() + if (it) { + viewPagerMain.setCurrentItem(bottomMenuCount - 1, false) + } + } + } + observeEvent(PreferKey.threadCount) { + viewModel.upPool() + } + } + + private fun upBottomMenu() { + val showDiscovery = AppConfig.showDiscovery + val showRss = AppConfig.showRSS + binding.bottomNavigationView.menu.let { menu -> + menu.findItem(R.id.menu_discovery).isVisible = showDiscovery + menu.findItem(R.id.menu_rss).isVisible = showRss + } + var index = 0 + if (showDiscovery) { + index++ + realPositions[index] = idExplore + } + if (showRss) { + index++ + realPositions[index] = idRss + } + index++ + realPositions[index] = idMy + bottomMenuCount = index + 1 + adapter.notifyDataSetChanged() + } + + private fun upHomePage() { + when (AppConfig.defaultHomePage) { + "bookshelf" -> {} + "explore" -> if (AppConfig.showDiscovery) { + binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idExplore), false) + } + + "rss" -> if (AppConfig.showRSS) { + binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idRss), false) + } + + "my" -> binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idMy), false) + } + } + + private fun getFragmentId(position: Int): Int { + val id = realPositions[position] + if (id == idBookshelf) { + return if (AppConfig.bookGroupStyle == 1) idBookshelf2 else idBookshelf1 + } + return id + } + + private inner class PageChangeCallback : ViewPager.SimpleOnPageChangeListener() { + + override fun onPageSelected(position: Int) { + pagePosition = position + binding.bottomNavigationView.menu[realPositions[position]].isChecked = true + } + + } + + @Suppress("DEPRECATION") + private inner class TabFragmentPageAdapter(fm: FragmentManager) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + private fun getId(position: Int): Int { + return getFragmentId(position) + } + + override fun getItemPosition(any: Any): Int { + val position = (any as MainFragmentInterface).position + ?: return POSITION_NONE + val fragmentId = getId(position) + if ((fragmentId == idBookshelf1 && any is BookshelfFragment1) + || (fragmentId == idBookshelf2 && any is BookshelfFragment2) + || (fragmentId == idExplore && any is ExploreFragment) + || (fragmentId == idRss && any is RssFragment) + || (fragmentId == idMy && any is MyFragment) + ) { + return POSITION_UNCHANGED + } + return POSITION_NONE + } + + override fun getItem(position: Int): Fragment { + return when (getId(position)) { + idBookshelf1 -> BookshelfFragment1(position) + idBookshelf2 -> BookshelfFragment2(position) + idExplore -> ExploreFragment(position) + idRss -> RssFragment(position) + else -> MyFragment(position) + } + } + + override fun getCount(): Int { + return bottomMenuCount + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + var fragment = super.instantiateItem(container, position) as Fragment + if (fragment.isCreated && getItemPosition(fragment) == POSITION_NONE) { + destroyItem(container, position, fragment) + fragment = super.instantiateItem(container, position) as Fragment + } + fragmentMap[getId(position)] = fragment + return fragment + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainFragmentInterface.kt b/app/src/main/java/io/legado/app/ui/main/MainFragmentInterface.kt new file mode 100755 index 000000000..6b11b08fc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/MainFragmentInterface.kt @@ -0,0 +1,7 @@ +package io.legado.app.ui.main + +interface MainFragmentInterface { + + val position: Int? + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt new file mode 100755 index 000000000..17540fe17 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -0,0 +1,249 @@ +package io.legado.app.ui.main + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import androidx.recyclerview.widget.RecyclerView.RecycledViewPool +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.help.AppWebDav +import io.legado.app.help.DefaultData +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.addType +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isUpError +import io.legado.app.help.book.removeType +import io.legado.app.help.book.sync +import io.legado.app.help.config.AppConfig +import io.legado.app.model.CacheBook +import io.legado.app.model.ReadBook +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.CacheBookService +import io.legado.app.utils.onEachParallel +import io.legado.app.utils.postEvent +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.LinkedList +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import kotlin.math.min + +class MainViewModel(application: Application) : BaseViewModel(application) { + private var threadCount = AppConfig.threadCount + private var poolSize = min(threadCount, AppConst.MAX_THREAD) + private var upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher() + private val waitUpTocBooks = LinkedList() + private val onUpTocBooks = ConcurrentHashMap.newKeySet() + val onUpBooksLiveData = MutableLiveData() + private var upTocJob: Job? = null + private var cacheBookJob: Job? = null + val booksListRecycledViewPool = RecycledViewPool().apply { + setMaxRecycledViews(0, 30) + } + val booksGridRecycledViewPool = RecycledViewPool().apply { + setMaxRecycledViews(0, 100) + } + + init { + deleteNotShelfBook() + } + + override fun onCleared() { + super.onCleared() + upTocPool.close() + } + + fun upPool() { + threadCount = AppConfig.threadCount + if (upTocJob?.isActive == true || cacheBookJob?.isActive == true) { + return + } + val newPoolSize = min(threadCount, AppConst.MAX_THREAD) + if (poolSize == newPoolSize) { + return + } + poolSize = newPoolSize + upTocPool.close() + upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher() + } + + fun isUpdate(bookUrl: String): Boolean { + return onUpTocBooks.contains(bookUrl) + } + + fun upAllBookToc() { + execute { + addToWaitUp(appDb.bookDao.hasUpdateBooks) + } + } + + fun upToc(books: List) { + execute(context = upTocPool) { + books.filter { + !it.isLocal && it.canUpdate + }.let { + addToWaitUp(it) + } + } + } + + @Synchronized + private fun addToWaitUp(books: List) { + books.forEach { book -> + if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) { + waitUpTocBooks.add(book.bookUrl) + } + } + if (upTocJob == null) { + startUpTocJob() + } + } + + private fun startUpTocJob() { + upPool() + postUpBooksLiveData() + upTocJob = viewModelScope.launch(upTocPool) { + flow { + while (true) { + emit(waitUpTocBooks.poll() ?: break) + } + }.onEachParallel(threadCount) { + onUpTocBooks.add(it) + postEvent(EventBus.UP_BOOKSHELF, it) + updateToc(it) + }.onEach { + onUpTocBooks.remove(it) + postEvent(EventBus.UP_BOOKSHELF, it) + postUpBooksLiveData() + }.onCompletion { + upTocJob = null + if (waitUpTocBooks.isNotEmpty()) { + startUpTocJob() + } + if (it == null && cacheBookJob == null && !CacheBookService.isRun) { + //所有目录更新完再开始缓存章节 + cacheBook() + } + }.catch { + AppLog.put("更新目录出错\n${it.localizedMessage}", it) + }.collect() + } + } + + private suspend fun updateToc(bookUrl: String) { + val book = appDb.bookDao.getBook(bookUrl) ?: return + val source = appDb.bookSourceDao.getBookSource(book.origin) + if (source == null) { + if (!book.isUpError) { + book.addType(BookType.updateError) + appDb.bookDao.update(book) + } + return + } + kotlin.runCatching { + val oldBook = book.copy() + if (book.tocUrl.isBlank()) { + WebBook.getBookInfoAwait(source, book) + } else { + WebBook.runPreUpdateJs(source, book) + } + val toc = WebBook.getChapterListAwait(source, book).getOrThrow() + book.sync(oldBook) + book.removeType(BookType.updateError) + if (book.bookUrl == bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + ReadBook.onChapterListUpdated(book) + addDownload(source, book) + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it) + //这里可能因为时间太长书籍信息已经更改,所以重新获取 + appDb.bookDao.getBook(book.bookUrl)?.let { book -> + book.addType(BookType.updateError) + appDb.bookDao.update(book) + } + } + } + + fun postUpBooksLiveData(reset: Boolean = false) { + if (AppConfig.showWaitUpCount) { + onUpBooksLiveData.postValue(waitUpTocBooks.size + onUpTocBooks.size) + } else if (reset) { + onUpBooksLiveData.postValue(0) + } + } + + @Synchronized + private fun addDownload(source: BookSource, book: Book) { + if (AppConfig.preDownloadNum == 0) return + val endIndex = min( + book.totalChapterNum - 1, + book.durChapterIndex.plus(AppConfig.preDownloadNum) + ) + val cacheBook = CacheBook.getOrCreate(source, book) + cacheBook.addDownload(book.durChapterIndex, endIndex) + } + + /** + * 缓存书籍 + */ + private fun cacheBook() { + if (AppConfig.preDownloadNum == 0) return + cacheBookJob?.cancel() + cacheBookJob = viewModelScope.launch(upTocPool) { + launch { + while (isActive && CacheBook.isRun) { + //有目录更新是不缓存,优先更新目录,现在更多网站限制并发 + CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty()) + delay(1000) + } + } + CacheBook.startProcessJob(upTocPool) + } + } + + fun postLoad() { + execute { + if (appDb.httpTTSDao.count == 0) { + DefaultData.httpTTS.let { + appDb.httpTTSDao.insert(*it.toTypedArray()) + } + } + } + } + + fun restoreWebDav(name: String) { + execute { + AppWebDav.restoreWebDav(name) + } + } + + private fun deleteNotShelfBook() { + execute { + appDb.bookDao.deleteNotShelfBook() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt new file mode 100755 index 000000000..4adcd0580 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt @@ -0,0 +1,263 @@ +package io.legado.app.ui.main.bookshelf + +import android.annotation.SuppressLint +import android.view.Menu +import android.view.MenuItem +import androidx.core.view.indices +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.LiveData +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.DialogBookshelfConfigBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.book.cache.CacheActivity +import io.legado.app.ui.book.group.GroupManageDialog +import io.legado.app.ui.book.import.local.ImportBookActivity +import io.legado.app.ui.book.import.remote.RemoteBookActivity +import io.legado.app.ui.book.manage.BookshelfManageActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.main.MainFragmentInterface +import io.legado.app.ui.main.MainViewModel +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.checkByIndex +import io.legado.app.utils.getCheckedIndex +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.postEvent +import io.legado.app.utils.readText +import io.legado.app.utils.sendToClip +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.toastOnUi + +abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment(layoutId), + MainFragmentInterface { + + override val position: Int? get() = arguments?.getInt("position") + + val activityViewModel by activityViewModels() + override val viewModel by viewModels() + + private val importBookshelf = registerForActivityResult(HandleFileContract()) { + kotlin.runCatching { + it.uri?.readText(requireContext())?.let { text -> + viewModel.importBookshelf(text, groupId) + } + }.onFailure { + toastOnUi(it.localizedMessage ?: "ERROR") + } + } + private val exportResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + requireContext().sendToClip(uri.toString()) + } + } + } + } + abstract val groupId: Long + abstract val books: List + private var groupsLiveData: LiveData>? = null + private val waitDialog by lazy { + WaitDialog(requireContext()).apply { + setOnCancelListener { + viewModel.addBookJob?.cancel() + } + } + } + + abstract fun gotoTop() + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_bookshelf, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + when (item.itemId) { + R.id.menu_remote -> startActivity() + R.id.menu_search -> startActivity() + R.id.menu_update_toc -> activityViewModel.upToc(books) + R.id.menu_bookshelf_layout -> configBookshelf() + R.id.menu_group_manage -> showDialogFragment() + R.id.menu_add_local -> startActivity() + R.id.menu_add_url -> showAddBookByUrlAlert() + R.id.menu_bookshelf_manage -> startActivity { + putExtra("groupId", groupId) + } + + R.id.menu_download -> startActivity { + putExtra("groupId", groupId) + } + + R.id.menu_export_bookshelf -> viewModel.exportBookshelf(books) { file -> + exportResult.launch { + mode = HandleFileContract.EXPORT + fileData = + HandleFileContract.FileData("bookshelf.json", file, "application/json") + } + } + + R.id.menu_import_bookshelf -> importBookshelfAlert(groupId) + R.id.menu_log -> showDialogFragment() + } + } + + protected fun initBookGroupData() { + groupsLiveData?.removeObservers(viewLifecycleOwner) + groupsLiveData = appDb.bookGroupDao.show.apply { + observe(viewLifecycleOwner) { + upGroup(it) + } + } + } + + abstract fun upGroup(data: List) + + abstract fun upSort() + + override fun observeLiveBus() { + viewModel.addBookProgressLiveData.observe(this) { count -> + if (count < 0) { + waitDialog.dismiss() + } else { + waitDialog.setText("添加中... ($count)") + } + } + } + + @SuppressLint("InflateParams") + fun showAddBookByUrlAlert() { + alert(titleResource = R.string.add_book_url) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + waitDialog.setText("添加中...") + waitDialog.show() + viewModel.addBookByUrl(it) + } + } + cancelButton() + } + } + + @SuppressLint("InflateParams") + fun configBookshelf() { + alert(titleResource = R.string.bookshelf_layout) { + var bookshelfLayout = AppConfig.bookshelfLayout + var bookshelfSort = AppConfig.bookshelfSort + val alertBinding = + DialogBookshelfConfigBinding.inflate(layoutInflater) + .apply { + if (AppConfig.bookGroupStyle !in 0..? = null + + fun addBookByUrl(bookUrls: String) { + var successCount = 0 + addBookJob = execute { + val hasBookUrlPattern: List by lazy { + appDb.bookSourceDao.hasBookUrlPattern + } + val urls = bookUrls.split("\n") + for (url in urls) { + val bookUrl = url.trim() + if (bookUrl.isEmpty()) continue + if (appDb.bookDao.getBook(bookUrl) != null) { + successCount++ + continue + } + val baseUrl = NetworkUtils.getBaseUrl(bookUrl) ?: continue + var source = appDb.bookSourceDao.getBookSourceAddBook(baseUrl) + if (source == null) { + for (bookSource in hasBookUrlPattern) { + try { + val bs = bookSource.getBookSource()!! + if (bookUrl.matches(bs.bookUrlPattern!!.toRegex())) { + source = bs + break + } + } catch (_: Exception) { + } + } + } + val bookSource = source ?: continue + val book = Book( + bookUrl = bookUrl, + origin = bookSource.bookSourceUrl, + originName = bookSource.bookSourceName + ) + kotlin.runCatching { + WebBook.getBookInfoAwait(bookSource, book) + }.onSuccess { + val dbBook = appDb.bookDao.getBook(it.name, it.author) + if (dbBook != null) { + val toc = WebBook.getChapterListAwait(bookSource, it).getOrThrow() + dbBook.migrateTo(it, toc) + appDb.bookDao.insert(it) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + } else { + it.order = appDb.bookDao.minOrder - 1 + it.save() + } + successCount++ + addBookProgressLiveData.postValue(successCount) + } + } + }.onSuccess { + if (successCount > 0) { + context.toastOnUi(R.string.success) + } else { + context.toastOnUi("添加网址失败") + } + }.onError { + AppLog.put("添加网址出错\n${it.localizedMessage}", it, true) + }.onFinally { + addBookProgressLiveData.postValue(-1) + } + } + + fun exportBookshelf(books: List?, success: (file: File) -> Unit) { + execute { + books?.let { + val path = "${context.filesDir}/books.json" + FileUtils.delete(path) + val file = FileUtils.createFileWithReplace(path) + FileOutputStream(file).use { out -> + val writer = JsonWriter(OutputStreamWriter(out, "UTF-8")) + writer.setIndent(" ") + writer.beginArray() + books.forEach { + val bookMap = hashMapOf() + bookMap["name"] = it.name + bookMap["author"] = it.author + bookMap["intro"] = it.getDisplayIntro() + GSON.toJson(bookMap, bookMap::class.java, writer) + } + writer.endArray() + writer.close() + } + file + } ?: throw NoStackTraceException("书籍不能为空") + }.onSuccess { + success(it) + }.onError { + context.toastOnUi("导出书籍出错\n${it.localizedMessage}") + } + } + + fun importBookshelf(str: String, groupId: Long) { + execute { + val text = str.trim() + when { + text.isAbsUrl() -> { + okHttpClient.newCallResponseBody { + url(text) + }.decompressed().text().let { + importBookshelf(it, groupId) + } + } + + text.isJsonArray() -> { + importBookshelfByJson(text, groupId) + } + + else -> { + throw NoStackTraceException("格式不对") + } + } + }.onError { + context.toastOnUi(it.localizedMessage ?: "ERROR") + } + } + + private fun importBookshelfByJson(json: String, groupId: Long) { + execute { + val bookSourceParts = appDb.bookSourceDao.allEnabledPart + val semaphore = Semaphore(AppConfig.threadCount) + GSON.fromJsonArray>(json).getOrThrow().forEach { bookInfo -> + val name = bookInfo["name"] ?: "" + val author = bookInfo["author"] ?: "" + if (name.isEmpty() || appDb.bookDao.has(name, author)) { + return@forEach + } + semaphore.withPermit { + WebBook.preciseSearch( + this, bookSourceParts, name, author, + semaphore = semaphore + ).onSuccess { + val book = it.first + if (groupId > 0) { + book.group = groupId + } + book.save() + }.onError { e -> + context.toastOnUi(e.localizedMessage) + } + } + } + }.onError { + it.printOnDebug() + }.onFinally { + context.toastOnUi(R.string.success) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt new file mode 100755 index 000000000..e99c2ff5b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/BookshelfFragment1.kt @@ -0,0 +1,188 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.ui.main.bookshelf.style1 + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import com.google.android.material.tabs.TabLayout +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.FragmentBookshelf1Binding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.group.GroupEditDialog +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment +import io.legado.app.ui.main.bookshelf.style1.books.BooksFragment +import io.legado.app.utils.isCreated +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlin.collections.set + +/** + * 书架界面 + */ +class BookshelfFragment1() : BaseBookshelfFragment(R.layout.fragment_bookshelf1), + TabLayout.OnTabSelectedListener, + SearchView.OnQueryTextListener { + + constructor(position: Int) : this() { + val bundle = Bundle() + bundle.putInt("position", position) + arguments = bundle + } + + private val binding by viewBinding(FragmentBookshelf1Binding::bind) + private val adapter by lazy { TabFragmentPageAdapter(childFragmentManager) } + private val tabLayout: TabLayout by lazy { + binding.titleBar.findViewById(R.id.tab_layout) + } + private val bookGroups = mutableListOf() + private val fragmentMap = hashMapOf() + override val groupId: Long get() = selectedGroup?.groupId ?: 0 + + override val books: List + get() { + val fragment = fragmentMap[groupId] + return fragment?.getBooks() ?: emptyList() + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + initView() + initBookGroupData() + } + + private val selectedGroup: BookGroup? + get() = bookGroups.getOrNull(tabLayout.selectedTabPosition) + + private fun initView() { + binding.viewPagerBookshelf.setEdgeEffectColor(primaryColor) + tabLayout.isTabIndicatorFullWidth = false + tabLayout.tabMode = TabLayout.MODE_SCROLLABLE + tabLayout.setSelectedTabIndicatorColor(requireContext().accentColor) + tabLayout.setupWithViewPager(binding.viewPagerBookshelf) + binding.viewPagerBookshelf.offscreenPageLimit = 1 + binding.viewPagerBookshelf.adapter = adapter + } + + override fun onQueryTextSubmit(query: String?): Boolean { + SearchActivity.start(requireContext(), query) + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + + @Synchronized + override fun upGroup(data: List) { + if (data.isEmpty()) { + appDb.bookGroupDao.enableGroup(BookGroup.IdAll) + } else { + if (data != bookGroups) { + bookGroups.clear() + bookGroups.addAll(data) + adapter.notifyDataSetChanged() + selectLastTab() + for (i in 0 until adapter.count) { + tabLayout.getTabAt(i)?.view?.setOnLongClickListener { + showDialogFragment(GroupEditDialog(bookGroups[i])) + true + } + } + } + } + } + + override fun upSort() { + adapter.notifyDataSetChanged() + } + + private fun selectLastTab() { + tabLayout.post { + tabLayout.removeOnTabSelectedListener(this) + tabLayout.getTabAt(AppConfig.saveTabPosition)?.select() + tabLayout.addOnTabSelectedListener(this) + } + } + + override fun onTabReselected(tab: TabLayout.Tab) { + selectedGroup?.let { group -> + fragmentMap[group.groupId]?.let { + toastOnUi("${group.groupName}(${it.getBooksCount()})") + } + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) = Unit + + override fun onTabSelected(tab: TabLayout.Tab) { + AppConfig.saveTabPosition = tab.position + } + + override fun gotoTop() { + fragmentMap[groupId]?.gotoTop() + } + + private inner class TabFragmentPageAdapter(fm: FragmentManager) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getPageTitle(position: Int): CharSequence { + return bookGroups[position].groupName + } + + /** + * 确定视图位置是否更改时调用 + * @return POSITION_NONE 已更改,刷新视图. POSITION_UNCHANGED 未更改,不刷新视图 + */ + override fun getItemPosition(any: Any): Int { + val fragment = any as BooksFragment + val position = fragment.position + val group = bookGroups.getOrNull(position) + if (fragment.groupId != group?.groupId) { + return POSITION_NONE + } + val bookSort = group.getRealBookSort() + fragment.setEnableRefresh(group.enableRefresh) + if (fragment.bookSort != bookSort) { + fragment.upBookSort(bookSort) + } + return POSITION_UNCHANGED + } + + override fun getItem(position: Int): Fragment { + val group = bookGroups[position] + return BooksFragment(position, group) + } + + override fun getCount(): Int { + return bookGroups.size + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + var fragment = super.instantiateItem(container, position) as BooksFragment + val group = bookGroups[position] + /** + * Activity recreate 会复用之前的 Fragment,不正确的需要重新创建 + */ + if (fragment.isCreated && getItemPosition(fragment) == POSITION_NONE) { + destroyItem(container, position, fragment) + fragment = super.instantiateItem(container, position) as BooksFragment + } + fragmentMap[group.groupId] = fragment + return fragment + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt new file mode 100755 index 000000000..6814f6232 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt @@ -0,0 +1,95 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.content.Context +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.viewbinding.ViewBinding +import io.legado.app.base.adapter.DiffRecyclerAdapter +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.Book + +abstract class BaseBooksAdapter(context: Context) : + DiffRecyclerAdapter(context) { + + override val keepScrollPosition = true + + override val diffItemCallback: DiffUtil.ItemCallback = + object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean { + return oldItem.name == newItem.name + && oldItem.author == newItem.author + } + + override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean { + return when { + oldItem.durChapterTime != newItem.durChapterTime -> false + oldItem.name != newItem.name -> false + oldItem.author != newItem.author -> false + oldItem.durChapterTitle != newItem.durChapterTitle -> false + oldItem.latestChapterTitle != newItem.latestChapterTitle -> false + oldItem.lastCheckCount != newItem.lastCheckCount -> false + oldItem.getDisplayCover() != newItem.getDisplayCover() -> false + oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false + else -> true + } + } + + override fun getChangePayload(oldItem: Book, newItem: Book): Any? { + val bundle = bundleOf() + if (oldItem.name != newItem.name) { + bundle.putString("name", newItem.name) + } + if (oldItem.author != newItem.author) { + bundle.putString("author", newItem.author) + } + if (oldItem.durChapterTitle != newItem.durChapterTitle) { + bundle.putString("dur", newItem.durChapterTitle) + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + bundle.putString("last", newItem.latestChapterTitle) + } + if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { + bundle.putString("cover", newItem.getDisplayCover()) + } + if (oldItem.lastCheckCount != newItem.lastCheckCount + || oldItem.durChapterTime != newItem.durChapterTime + || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() + || oldItem.lastCheckCount != newItem.lastCheckCount + ) { + bundle.putBoolean("refresh", true) + } + if (oldItem.latestChapterTime != newItem.latestChapterTime) { + bundle.putBoolean("lastUpdateTime", true) + } + if (bundle.isEmpty) return null + return bundle + } + + } + + override fun onViewRecycled(holder: ItemViewHolder) { + super.onViewRecycled(holder) + holder.itemView.setOnClickListener(null) + holder.itemView.setOnLongClickListener(null) + } + + fun notification(bookUrl: String) { + getItems().forEachIndexed { i, it -> + if (it.bookUrl == bookUrl) { + notifyItemChanged(i, bundleOf(Pair("refresh", null), Pair("lastUpdateTime", null))) + return + } + } + } + + fun upLastUpdateTime() { + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("lastUpdateTime", null))) + } + + interface CallBack { + fun open(book: Book) + fun openBookInfo(book: Book) + fun isUpdate(bookUrl: String): Boolean + } +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt new file mode 100755 index 000000000..302f1b466 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterGrid.kt @@ -0,0 +1,75 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ItemBookshelfGridBinding +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.invisible +import splitties.views.onLongClick + +class BooksAdapterGrid(context: Context, private val callBack: CallBack) : + BaseBooksAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridBinding { + return ItemBookshelfGridBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookshelfGridBinding, + item: Book, + payloads: MutableList + ) = binding.run { + if (payloads.isEmpty()) { + tvName.text = item.name + ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) + upRefresh(binding, item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "cover" -> ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) + "refresh" -> upRefresh(binding, item) + } + } + } + } + } + + private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) { + if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.visible() + } else { + binding.rlLoading.inVisible() + if (AppConfig.showUnread) { + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + } else { + binding.bvUnread.invisible() + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfGridBinding) { + holder.itemView.apply { + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.open(it) + } + } + + onLongClick { + getItem(holder.layoutPosition)?.let { + callBack.openBookInfo(it) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt new file mode 100755 index 000000000..740930adb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksAdapterList.kt @@ -0,0 +1,110 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.Book +import io.legado.app.databinding.ItemBookshelfListBinding +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.invisible +import io.legado.app.utils.toTimeAgo +import splitties.views.onLongClick + +class BooksAdapterList( + context: Context, + private val fragment: Fragment, + private val callBack: CallBack, + private val lifecycle: Lifecycle +) : BaseBooksAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookshelfListBinding { + return ItemBookshelfListBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookshelfListBinding, + item: Book, + payloads: MutableList + ) = binding.run { + if (payloads.isEmpty()) { + tvName.text = item.name + tvAuthor.text = item.author + tvRead.text = item.durChapterTitle + tvLast.text = item.latestChapterTitle + ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) + upRefresh(binding, item) + upLastUpdateTime(binding, item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "author" -> tvAuthor.text = item.author + "dur" -> tvRead.text = item.durChapterTitle + "last" -> tvLast.text = item.latestChapterTitle + "cover" -> ivCover.load( + item.getDisplayCover(), + item.name, + item.author, + false, + item.origin, + fragment, + lifecycle + ) + + "refresh" -> upRefresh(binding, item) + "lastUpdateTime" -> upLastUpdateTime(binding, item) + } + } + } + } + } + + private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) { + if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.visible() + } else { + binding.rlLoading.gone() + if (AppConfig.showUnread) { + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + } else { + binding.bvUnread.invisible() + } + } + } + + private fun upLastUpdateTime(binding: ItemBookshelfListBinding, item: Book) { + if (AppConfig.showLastUpdateTime && !item.isLocal) { + val time = item.latestChapterTime.toTimeAgo() + if (binding.tvLastUpdateTime.text != time) { + binding.tvLastUpdateTime.text = time + } + } else { + binding.tvLastUpdateTime.text = "" + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfListBinding) { + holder.itemView.apply { + setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.open(it) + } + } + + onLongClick { + getItem(holder.layoutPosition)?.let { + callBack.openBookInfo(it) + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt new file mode 100755 index 000000000..43372aabe --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt @@ -0,0 +1,268 @@ +package io.legado.app.ui.main.bookshelf.style1.books + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.view.ViewConfiguration +import androidx.core.view.isGone +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy +import io.legado.app.R +import io.legado.app.base.BaseFragment +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.FragmentBooksBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.main.MainViewModel +import io.legado.app.utils.cnCompare +import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst +import io.legado.app.utils.observeEvent +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.startActivity +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.math.max + +/** + * 书架界面 + */ +class BooksFragment() : BaseFragment(R.layout.fragment_books), + BaseBooksAdapter.CallBack { + + constructor(position: Int, group: BookGroup) : this() { + val bundle = Bundle() + bundle.putInt("position", position) + bundle.putLong("groupId", group.groupId) + bundle.putInt("bookSort", group.getRealBookSort()) + bundle.putBoolean("enableRefresh", group.enableRefresh) + arguments = bundle + } + + private val binding by viewBinding(FragmentBooksBinding::bind) + private val activityViewModel by activityViewModels() + private val bookshelfLayout by lazy { AppConfig.bookshelfLayout } + private val booksAdapter: BaseBooksAdapter<*> by lazy { + if (bookshelfLayout == 0) { + BooksAdapterList(requireContext(), this, this, viewLifecycleOwner.lifecycle) + } else { + BooksAdapterGrid(requireContext(), this) + } + } + private var booksFlowJob: Job? = null + var position = 0 + private set + var groupId = -1L + private set + var bookSort = 0 + private set + private var upLastUpdateTimeJob: Job? = null + private var enableRefresh = true + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + arguments?.let { + position = it.getInt("position", 0) + groupId = it.getLong("groupId", -1) + bookSort = it.getInt("bookSort", 0) + enableRefresh = it.getBoolean("enableRefresh", true) + binding.refreshLayout.isEnabled = enableRefresh + } + initRecyclerView() + upRecyclerData() + } + + private fun initRecyclerView() { + binding.rvBookshelf.setEdgeEffectColor(primaryColor) + upFastScrollerBar() + binding.refreshLayout.setColorSchemeColors(accentColor) + binding.refreshLayout.setOnRefreshListener { + binding.refreshLayout.isRefreshing = false + activityViewModel.upToc(booksAdapter.getItems()) + } + if (bookshelfLayout == 0) { + binding.rvBookshelf.layoutManager = LinearLayoutManager(context) + } else { + binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2) + } + if (bookshelfLayout == 0) { + binding.rvBookshelf.setRecycledViewPool(activityViewModel.booksListRecycledViewPool) + } else { + binding.rvBookshelf.setRecycledViewPool(activityViewModel.booksGridRecycledViewPool) + } + booksAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + binding.rvBookshelf.adapter = booksAdapter + booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (positionStart == 0 && itemCount == 1 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (toPosition == 0 && itemCount == 1 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + }) + startLastUpdateTimeJob() + } + + private fun upFastScrollerBar() { + val showBookshelfFastScroller = AppConfig.showBookshelfFastScroller + binding.rvBookshelf.setFastScrollEnabled(showBookshelfFastScroller) + if (showBookshelfFastScroller) { + binding.rvBookshelf.scrollBarSize = 0 + } else { + binding.rvBookshelf.scrollBarSize = + ViewConfiguration.get(requireContext()).scaledScrollBarSize + } + } + + fun upBookSort(sort: Int) { + binding.root.post { + arguments?.putInt("bookSort", sort) + bookSort = sort + upRecyclerData() + } + } + + fun setEnableRefresh(enable: Boolean) { + enableRefresh = enable + binding.refreshLayout.isEnabled = enable + } + + /** + * 更新书籍列表信息 + */ + private fun upRecyclerData() { + booksFlowJob?.cancel() + booksFlowJob = viewLifecycleOwner.lifecycleScope.launch { + appDb.bookDao.flowByGroup(groupId).map { list -> + //排序 + when (bookSort) { + 1 -> list.sortedByDescending { it.latestChapterTime } + 2 -> list.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + + 3 -> list.sortedBy { it.order } + + // 综合排序 issue #3192 + 4 -> list.sortedByDescending { + max(it.latestChapterTime, it.durChapterTime) + } + // 按作者排序 + 5 -> list.sortedWith { o1, o2 -> + o1.author.cnCompare(o2.author) + } + + else -> list.sortedByDescending { it.durChapterTime } + } + }.flowWithLifecycleAndDatabaseChangeFirst( + viewLifecycleOwner.lifecycle, + Lifecycle.State.RESUMED, + AppDatabase.BOOK_TABLE_NAME + ).catch { + AppLog.put("书架更新出错", it) + }.conflate().flowOn(Dispatchers.Default).collect { list -> + binding.tvEmptyMsg.isGone = list.isNotEmpty() + binding.refreshLayout.isEnabled = enableRefresh && list.isNotEmpty() + booksAdapter.setItems(list) + delay(100) + } + } + } + + private fun startLastUpdateTimeJob() { + upLastUpdateTimeJob?.cancel() + if (!AppConfig.showLastUpdateTime || bookshelfLayout != 0) { + return + } + upLastUpdateTimeJob = viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + while (isActive) { + booksAdapter.upLastUpdateTime() + delay(30 * 1000) + } + } + } + } + + fun getBooks(): List { + return booksAdapter.getItems() + } + + fun gotoTop() { + if (AppConfig.isEInkMode) { + binding.rvBookshelf.scrollToPosition(0) + } else { + binding.rvBookshelf.smoothScrollToPosition(0) + } + } + + fun getBooksCount(): Int { + return booksAdapter.itemCount + } + + override fun onDestroyView() { + super.onDestroyView() + /** + * 将 RecyclerView 中的视图全部回收到 RecycledViewPool 中 + */ + binding.rvBookshelf.setItemViewCacheSize(0) + binding.rvBookshelf.adapter = null + } + + override fun open(book: Book) { + startActivityForBook(book) + } + + override fun openBookInfo(book: Book) { + startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + } + } + + override fun isUpdate(bookUrl: String): Boolean { + return activityViewModel.isUpdate(bookUrl) + } + + @SuppressLint("NotifyDataSetChanged") + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.UP_BOOKSHELF) { + booksAdapter.notification(it) + } + observeEvent(EventBus.BOOKSHELF_REFRESH) { + booksAdapter.notifyDataSetChanged() + startLastUpdateTimeJob() + upFastScrollerBar() + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt new file mode 100755 index 000000000..a6965c72f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BaseBooksAdapter.kt @@ -0,0 +1,143 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.content.Context +import android.view.LayoutInflater +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup + +abstract class BaseBooksAdapter( + val context: Context, + val callBack: CallBack +) : RecyclerView.Adapter() { + + protected val inflater: LayoutInflater = LayoutInflater.from(context) + + private val diffItemCallback = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean { + return when { + oldItem is Book && newItem is Book -> { + oldItem.name == newItem.name + && oldItem.author == newItem.author + } + + oldItem is BookGroup && newItem is BookGroup -> { + oldItem.groupId == newItem.groupId + } + + else -> false + } + } + + override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean { + return when { + oldItem is Book && newItem is Book -> { + oldItem.durChapterTime == newItem.durChapterTime && + oldItem.name == newItem.name && + oldItem.author == newItem.author && + oldItem.durChapterTitle == newItem.durChapterTitle && + oldItem.latestChapterTitle == newItem.latestChapterTitle && + oldItem.lastCheckCount == newItem.lastCheckCount && + oldItem.getDisplayCover() == newItem.getDisplayCover() && + oldItem.getUnreadChapterNum() == newItem.getUnreadChapterNum() + } + + oldItem is BookGroup && newItem is BookGroup -> { + oldItem.groupName == newItem.groupName && + oldItem.cover == newItem.cover + } + + else -> false + } + } + + override fun getChangePayload(oldItem: Any, newItem: Any): Any? { + val bundle = bundleOf() + when { + oldItem is Book && newItem is Book -> { + if (oldItem.name != newItem.name) { + bundle.putString("name", newItem.name) + } + if (oldItem.author != newItem.author) { + bundle.putString("author", newItem.author) + } + if (oldItem.durChapterTitle != newItem.durChapterTitle) { + bundle.putString("dur", newItem.durChapterTitle) + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + bundle.putString("last", newItem.latestChapterTitle) + } + if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { + bundle.putString("cover", newItem.getDisplayCover()) + } + if (oldItem.lastCheckCount != newItem.lastCheckCount + || oldItem.durChapterTime != newItem.durChapterTime + || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() + || oldItem.lastCheckCount != newItem.lastCheckCount + ) { + bundle.putBoolean("refresh", true) + } + } + + oldItem is BookGroup && newItem is BookGroup -> { + if (oldItem.groupName != newItem.groupName) { + bundle.putString("groupName", newItem.groupName) + } + if (oldItem.cover != newItem.cover) { + bundle.putString("cover", newItem.cover) + } + } + } + if (bundle.isEmpty) return null + return bundle + } + } + + private val asyncListDiffer by lazy { + AsyncListDiffer(this, diffItemCallback) + } + + fun updateItems() { + asyncListDiffer.submitList(callBack.getItems()) + } + + fun notification(bookUrl: String) { + for (i in 0 until itemCount) { + getItem(i).let { + if (it is Book && it.bookUrl == bookUrl) { + notifyItemChanged(i, bundleOf(Pair("refresh", null))) + return + } + } + } + } + + fun getItems() = asyncListDiffer.currentList + + fun getItem(position: Int) = getItems().getOrNull(position) + + override fun getItemCount(): Int { + return getItems().size + } + + override fun getItemViewType(position: Int): Int { + if (getItem(position) is BookGroup) { + return 1 + } + return 0 + } + + final override fun onBindViewHolder(holder: VH, position: Int) {} + + + interface CallBack { + fun onItemClick(item: Any) + fun onItemLongClick(item: Any) + fun isUpdate(bookUrl: String): Boolean + fun getItems(): List + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt new file mode 100755 index 000000000..569f77afc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt @@ -0,0 +1,142 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ItemBookshelfGridBinding +import io.legado.app.databinding.ItemBookshelfGridGroupBinding +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.invisible +import splitties.views.onLongClick + +@Suppress("UNUSED_PARAMETER") +class BooksAdapterGrid(context: Context, callBack: CallBack) : + BaseBooksAdapter(context, callBack) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + 1 -> GroupViewHolder(ItemBookshelfGridGroupBinding.inflate(inflater, parent, false)) + else -> BookViewHolder(ItemBookshelfGridBinding.inflate(inflater, parent, false)) + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + when (holder) { + is BookViewHolder -> (getItem(position) as? Book)?.let { + holder.registerListener(it) + holder.onBind(it, position, payloads) + } + + is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { + holder.registerListener(it) + holder.onBind(it, position, payloads) + } + } + } + + inner class BookViewHolder(val binding: ItemBookshelfGridBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun onBind(item: Book, position: Int) = binding.run { + tvName.text = item.name + ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) + upRefresh(this, item) + } + + fun onBind(item: Book, position: Int, payloads: MutableList) = binding.run { + if (payloads.isEmpty()) { + onBind(item, position) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "cover" -> ivCover.load( + item.getDisplayCover(), + item.name, + item.author, + false, + item.origin + ) + + "refresh" -> upRefresh(this, item) + } + } + } + } + } + + fun registerListener(item: Any) { + binding.root.setOnClickListener { + callBack.onItemClick(item) + } + binding.root.onLongClick { + callBack.onItemLongClick(item) + } + } + + private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) { + if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.visible() + } else { + binding.rlLoading.inVisible() + if (AppConfig.showUnread) { + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + } else { + binding.bvUnread.invisible() + } + } + } + + } + + inner class GroupViewHolder(val binding: ItemBookshelfGridGroupBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun onBind(item: BookGroup, position: Int) = binding.run { + tvName.text = item.groupName + ivCover.load(item.cover) + } + + fun onBind(item: BookGroup, position: Int, payloads: MutableList) = binding.run { + if (payloads.isEmpty()) { + onBind(item, position) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "groupName" -> tvName.text = item.groupName + "cover" -> ivCover.load(item.cover) + } + } + } + } + } + + fun registerListener(item: Any) { + binding.root.setOnClickListener { + callBack.onItemClick(item) + } + binding.root.onLongClick { + callBack.onItemLongClick(item) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt new file mode 100755 index 000000000..d15b752c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt @@ -0,0 +1,158 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.ItemBookshelfListBinding +import io.legado.app.databinding.ItemBookshelfListGroupBinding +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.visible +import splitties.views.onLongClick + +@Suppress("UNUSED_PARAMETER") +class BooksAdapterList(context: Context, callBack: CallBack) : + BaseBooksAdapter(context, callBack) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + 1 -> GroupViewHolder(ItemBookshelfListGroupBinding.inflate(inflater, parent, false)) + else -> BookViewHolder(ItemBookshelfListBinding.inflate(inflater, parent, false)) + } + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: MutableList + ) { + when (holder) { + is BookViewHolder -> (getItem(position) as? Book)?.let { + holder.registerListener(it) + holder.onBind(it, position, payloads) + } + + is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { + holder.registerListener(it) + holder.onBind(it, position, payloads) + } + } + } + + inner class BookViewHolder(val binding: ItemBookshelfListBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun onBind(item: Book, position: Int) = binding.run { + tvName.text = item.name + tvAuthor.text = item.author + tvRead.text = item.durChapterTitle + tvLast.text = item.latestChapterTitle + ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) + flHasNew.visible() + ivAuthor.visible() + ivLast.visible() + ivRead.visible() + upRefresh(this, item) + } + + fun onBind(item: Book, position: Int, payloads: MutableList) = binding.run { + if (payloads.isEmpty()) { + onBind(item, position) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "name" -> tvName.text = item.name + "author" -> tvAuthor.text = item.author + "dur" -> tvRead.text = item.durChapterTitle + "last" -> tvLast.text = item.latestChapterTitle + "cover" -> ivCover.load( + item.getDisplayCover(), + item.name, + item.author, + false, + item.origin + ) + + "refresh" -> upRefresh(this, item) + } + } + } + } + } + + fun registerListener(item: Any) { + binding.root.setOnClickListener { + callBack.onItemClick(item) + } + binding.root.onLongClick { + callBack.onItemLongClick(item) + } + } + + private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) { + if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { + binding.bvUnread.invisible() + binding.rlLoading.visible() + } else { + binding.rlLoading.gone() + if (AppConfig.showUnread) { + binding.bvUnread.setHighlight(item.lastCheckCount > 0) + binding.bvUnread.setBadgeCount(item.getUnreadChapterNum()) + } else { + binding.bvUnread.invisible() + } + } + } + + } + + inner class GroupViewHolder(val binding: ItemBookshelfListGroupBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun onBind(item: BookGroup, position: Int) = binding.run { + tvName.text = item.groupName + ivCover.load(item.cover) + flHasNew.gone() + ivAuthor.gone() + ivLast.gone() + ivRead.gone() + tvAuthor.gone() + tvLast.gone() + tvRead.gone() + } + + fun onBind(item: BookGroup, position: Int, payloads: MutableList) = binding.run { + if (payloads.isEmpty()) { + onBind(item, position) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "groupName" -> tvName.text = item.groupName + "cover" -> ivCover.load(item.cover) + } + } + } + } + } + + fun registerListener(item: Any) { + binding.root.setOnClickListener { + callBack.onItemClick(item) + } + binding.root.onLongClick { + callBack.onItemLongClick(item) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt new file mode 100755 index 000000000..ae9bd379d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt @@ -0,0 +1,261 @@ +package io.legado.app.ui.main.bookshelf.style2 + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.databinding.FragmentBookshelf2Binding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.book.group.GroupEditDialog +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment +import io.legado.app.utils.cnCompare +import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst +import io.legado.app.utils.observeEvent +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.startActivityForBook +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlin.math.max + +/** + * 书架界面 + */ +class BookshelfFragment2() : BaseBookshelfFragment(R.layout.fragment_bookshelf2), + SearchView.OnQueryTextListener, + BaseBooksAdapter.CallBack { + + constructor(position: Int) : this() { + val bundle = Bundle() + bundle.putInt("position", position) + arguments = bundle + } + + private val binding by viewBinding(FragmentBookshelf2Binding::bind) + private val bookshelfLayout by lazy { AppConfig.bookshelfLayout } + private val booksAdapter: BaseBooksAdapter<*> by lazy { + if (bookshelfLayout == 0) { + BooksAdapterList(requireContext(), this) + } else { + BooksAdapterGrid(requireContext(), this) + } + } + private var bookGroups: List = emptyList() + private var booksFlowJob: Job? = null + override var groupId = BookGroup.IdRoot + override var books: List = emptyList() + private var enableRefresh = true + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + initRecyclerView() + initBookGroupData() + initBooksData() + } + + private fun initRecyclerView() { + binding.rvBookshelf.setEdgeEffectColor(primaryColor) + binding.refreshLayout.setColorSchemeColors(accentColor) + binding.refreshLayout.setOnRefreshListener { + binding.refreshLayout.isRefreshing = false + activityViewModel.upToc(books) + } + if (bookshelfLayout == 0) { + binding.rvBookshelf.layoutManager = LinearLayoutManager(context) + } else { + binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayout + 2) + } + binding.rvBookshelf.itemAnimator = null + binding.rvBookshelf.adapter = booksAdapter + booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (positionStart == 0 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + val layoutManager = binding.rvBookshelf.layoutManager + if (toPosition == 0 && layoutManager is LinearLayoutManager) { + val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount + binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) + } + } + }) + } + + override fun upGroup(data: List) { + if (data != bookGroups) { + bookGroups = data + booksAdapter.updateItems() + binding.tvEmptyMsg.isGone = getItemCount() > 0 + binding.refreshLayout.isEnabled = enableRefresh && getItemCount() > 0 + } + } + + override fun upSort() { + initBooksData() + } + + private fun initBooksData() { + if (groupId == BookGroup.IdRoot) { + if (isAdded) { + binding.titleBar.title = getString(R.string.bookshelf) + binding.refreshLayout.isEnabled = true + enableRefresh = true + } + } else { + bookGroups.firstOrNull { + groupId == it.groupId + }?.let { + binding.titleBar.title = "${getString(R.string.bookshelf)}(${it.groupName})" + binding.refreshLayout.isEnabled = it.enableRefresh + enableRefresh = it.enableRefresh + } + } + booksFlowJob?.cancel() + booksFlowJob = viewLifecycleOwner.lifecycleScope.launch { + appDb.bookDao.flowByGroup(groupId).map { list -> + //排序 + when (AppConfig.getBookSortByGroupId(groupId)) { + 1 -> list.sortedByDescending { + it.latestChapterTime + } + + 2 -> list.sortedWith { o1, o2 -> + o1.name.cnCompare(o2.name) + } + + 3 -> list.sortedBy { + it.order + } + + 4 -> list.sortedByDescending { + max(it.latestChapterTime, it.durChapterTime) + } + + else -> list.sortedByDescending { + it.durChapterTime + } + } + }.flowWithLifecycleAndDatabaseChangeFirst( + viewLifecycleOwner.lifecycle, + Lifecycle.State.RESUMED, + AppDatabase.BOOK_TABLE_NAME + ).catch { + AppLog.put("书架更新出错", it) + }.conflate().flowOn(Dispatchers.Default).collect { list -> + books = list + booksAdapter.updateItems() + binding.tvEmptyMsg.isGone = getItemCount() > 0 + binding.refreshLayout.isEnabled = enableRefresh && getItemCount() > 0 + delay(100) + } + } + } + + fun back(): Boolean { + if (groupId != BookGroup.IdRoot) { + groupId = BookGroup.IdRoot + initBooksData() + return true + } + return false + } + + override fun onQueryTextSubmit(query: String?): Boolean { + SearchActivity.start(requireContext(), query) + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + return false + } + + override fun gotoTop() { + if (AppConfig.isEInkMode) { + binding.rvBookshelf.scrollToPosition(0) + } else { + binding.rvBookshelf.smoothScrollToPosition(0) + } + } + + override fun onItemClick(item: Any) { + when (item) { + is Book -> startActivityForBook(item) + + is BookGroup -> { + groupId = item.groupId + initBooksData() + } + } + } + + override fun onItemLongClick(item: Any) { + when (item) { + is Book -> startActivity { + putExtra("name", item.name) + putExtra("author", item.author) + } + + is BookGroup -> showDialogFragment(GroupEditDialog(item)) + } + } + + override fun isUpdate(bookUrl: String): Boolean { + return activityViewModel.isUpdate(bookUrl) + } + + fun getItemCount(): Int { + return if (groupId == BookGroup.IdRoot) { + bookGroups.size + books.size + } else { + books.size + } + } + + override fun getItems(): List { + if (groupId != BookGroup.IdRoot) { + return books + } + return bookGroups + books + } + + @SuppressLint("NotifyDataSetChanged") + override fun observeLiveBus() { + super.observeLiveBus() + observeEvent(EventBus.UP_BOOKSHELF) { + booksAdapter.notification(it) + } + observeEvent(EventBus.BOOKSHELF_REFRESH) { + booksAdapter.notifyDataSetChanged() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt new file mode 100755 index 000000000..b2664cf03 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt @@ -0,0 +1,193 @@ +package io.legado.app.ui.main.explore + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import android.widget.TextView +import androidx.core.view.children +import com.google.android.flexbox.FlexboxLayout +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.data.entities.rule.ExploreKind +import io.legado.app.databinding.ItemFilletTextBinding +import io.legado.app.databinding.ItemFindBookBinding +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.source.clearExploreKindsCache +import io.legado.app.help.source.exploreKinds +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.activity +import io.legado.app.utils.dpToPx +import io.legado.app.utils.gone +import io.legado.app.utils.removeLastElement +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.startActivity +import io.legado.app.utils.visible +import kotlinx.coroutines.CoroutineScope +import splitties.views.onLongClick + +class ExploreAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + + private val recycler = arrayListOf() + private var exIndex = -1 + private var scrollTo = -1 + + override fun getViewBinding(parent: ViewGroup): ItemFindBookBinding { + return ItemFindBookBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFindBookBinding, + item: BookSourcePart, + payloads: MutableList + ) { + binding.run { + if (holder.layoutPosition == itemCount - 1) { + root.setPadding(16.dpToPx(), 12.dpToPx(), 16.dpToPx(), 12.dpToPx()) + } else { + root.setPadding(16.dpToPx(), 12.dpToPx(), 16.dpToPx(), 0) + } + if (payloads.isEmpty()) { + tvName.text = item.bookSourceName + } + if (exIndex == holder.layoutPosition) { + ivStatus.setImageResource(R.drawable.ic_arrow_down) + rotateLoading.loadingColor = context.accentColor + rotateLoading.visible() + if (scrollTo >= 0) { + callBack.scrollTo(scrollTo) + } + Coroutine.async(callBack.scope) { + item.exploreKinds() + }.onSuccess { kindList -> + upKindList(flexbox, item.bookSourceUrl, kindList) + }.onFinally { + rotateLoading.gone() + if (scrollTo >= 0) { + callBack.scrollTo(scrollTo) + scrollTo = -1 + } + } + } else kotlin.runCatching { + ivStatus.setImageResource(R.drawable.ic_arrow_right) + rotateLoading.gone() + recyclerFlexbox(flexbox) + flexbox.gone() + } + } + } + + private fun upKindList(flexbox: FlexboxLayout, sourceUrl: String, kinds: List) { + if (kinds.isNotEmpty()) kotlin.runCatching { + recyclerFlexbox(flexbox) + flexbox.visible() + kinds.forEach { kind -> + val tv = getFlexboxChild(flexbox) + flexbox.addView(tv) + tv.text = kind.title + kind.style().apply(tv) + if (kind.url.isNullOrBlank()) { + tv.setOnClickListener(null) + } else { + tv.setOnClickListener { + if (kind.title.startsWith("ERROR:")) { + it.activity?.showDialogFragment(TextDialog("ERROR", kind.url)) + } else { + callBack.openExplore(sourceUrl, kind.title, kind.url) + } + } + } + } + } + } + + @Synchronized + private fun getFlexboxChild(flexbox: FlexboxLayout): TextView { + return if (recycler.isEmpty()) { + ItemFilletTextBinding.inflate(inflater, flexbox, false).root + } else { + recycler.removeLastElement() as TextView + } + } + + @Synchronized + private fun recyclerFlexbox(flexbox: FlexboxLayout) { + recycler.addAll(flexbox.children) + flexbox.removeAllViews() + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFindBookBinding) { + binding.apply { + llTitle.setOnClickListener { + val position = holder.layoutPosition + val oldEx = exIndex + exIndex = if (exIndex == position) -1 else position + notifyItemChanged(oldEx, false) + if (exIndex != -1) { + scrollTo = position + callBack.scrollTo(position) + notifyItemChanged(position, false) + } + } + llTitle.onLongClick { + showMenu(llTitle, holder.layoutPosition) + } + } + } + + fun compressExplore(): Boolean { + return if (exIndex < 0) { + false + } else { + val oldExIndex = exIndex + exIndex = -1 + notifyItemChanged(oldExIndex) + true + } + } + + private fun showMenu(view: View, position: Int): Boolean { + val source = getItem(position) ?: return true + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.explore_item) + popupMenu.menu.findItem(R.id.menu_login).isVisible = source.hasLoginUrl + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_edit -> callBack.editSource(source.bookSourceUrl) + R.id.menu_top -> callBack.toTop(source) + R.id.menu_search -> callBack.searchBook(source) + R.id.menu_login -> context.startActivity { + putExtra("type", "bookSource") + putExtra("key", source.bookSourceUrl) + } + + R.id.menu_refresh -> Coroutine.async(callBack.scope) { + source.clearExploreKindsCache() + }.onSuccess { + notifyItemChanged(position) + } + + R.id.menu_del -> callBack.deleteSource(source) + } + true + } + popupMenu.show() + return true + } + + interface CallBack { + val scope: CoroutineScope + fun scrollTo(pos: Int) + fun openExplore(sourceUrl: String, title: String, exploreUrl: String?) + fun editSource(sourceUrl: String) + fun toTop(source: BookSourcePart) + fun deleteSource(source: BookSourcePart) + fun searchBook(bookSource: BookSourcePart) + } +} diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt new file mode 100755 index 000000000..164da8bc0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt @@ -0,0 +1,17 @@ +package io.legado.app.ui.main.explore + +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.data.entities.BookSourcePart + + +class ExploreDiffItemCallBack : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { + return oldItem.bookSourceName == newItem.bookSourceName + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt new file mode 100755 index 000000000..c2d36f99a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -0,0 +1,240 @@ +package io.legado.app.ui.main.explore + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import android.view.View +import androidx.appcompat.widget.SearchView +import androidx.core.view.isGone +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppLog +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.databinding.FragmentExploreBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.book.explore.ExploreShowActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.book.search.SearchScope +import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.main.MainFragmentInterface +import io.legado.app.utils.applyTint +import io.legado.app.utils.flowWithLifecycleAndDatabaseChange +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.startActivity +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 发现界面 + */ +class ExploreFragment() : VMBaseFragment(R.layout.fragment_explore), + MainFragmentInterface, + ExploreAdapter.CallBack { + + constructor(position: Int) : this() { + val bundle = Bundle() + bundle.putInt("position", position) + arguments = bundle + } + + override val position: Int? get() = arguments?.getInt("position") + + override val viewModel by viewModels() + private val binding by viewBinding(FragmentExploreBinding::bind) + private val adapter by lazy { ExploreAdapter(requireContext(), this) } + private val linearLayoutManager by lazy { LinearLayoutManager(context) } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private val diffItemCallBack = ExploreDiffItemCallBack() + private val groups = linkedSetOf() + private var exploreFlowJob: Job? = null + private var groupsMenu: SubMenu? = null + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + initSearchView() + initRecyclerView() + initGroupData() + upExploreData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + super.onCompatCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.main_explore, menu) + groupsMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupsMenu() + } + + override fun onPause() { + super.onPause() + searchView.clearFocus() + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.screen_find) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + upExploreData(newText) + return false + } + }) + } + + private fun initRecyclerView() { + binding.rvFind.setEdgeEffectColor(primaryColor) + binding.rvFind.layoutManager = linearLayoutManager + binding.rvFind.adapter = adapter + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + if (positionStart == 0) { + binding.rvFind.scrollToPosition(0) + } + } + }) + } + + private fun initGroupData() { + viewLifecycleOwner.lifecycleScope.launch { + appDb.bookSourceDao.flowExploreGroups() + .flowWithLifecycleAndDatabaseChange( + viewLifecycleOwner.lifecycle, + Lifecycle.State.RESUMED, + AppDatabase.BOOK_SOURCE_TABLE_NAME + ) + .conflate() + .distinctUntilChanged() + .collect { + groups.clear() + groups.addAll(it) + upGroupsMenu() + delay(500) + } + } + } + + private fun upExploreData(searchKey: String? = null) { + exploreFlowJob?.cancel() + exploreFlowJob = viewLifecycleOwner.lifecycleScope.launch { + when { + searchKey.isNullOrBlank() -> { + appDb.bookSourceDao.flowExplore() + } + + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.bookSourceDao.flowGroupExplore(key) + } + + else -> { + appDb.bookSourceDao.flowExplore(searchKey) + } + }.flowWithLifecycleAndDatabaseChange( + viewLifecycleOwner.lifecycle, + Lifecycle.State.RESUMED, + AppDatabase.BOOK_SOURCE_TABLE_NAME + ).catch { + AppLog.put("发现界面更新数据出错", it) + }.conflate().flowOn(IO).collect { + binding.tvEmptyMsg.isGone = it.isNotEmpty() || searchView.query.isNotEmpty() + adapter.setItems(it, diffItemCallBack) + delay(500) + } + } + } + + private fun upGroupsMenu() = groupsMenu?.transaction { subMenu -> + subMenu.removeGroup(R.id.menu_group_text) + groups.forEach { + subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it) + } + } + + override val scope: CoroutineScope + get() = viewLifecycleOwner.lifecycleScope + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + if (item.groupId == R.id.menu_group_text) { + searchView.setQuery("group:${item.title}", true) + } + } + + override fun scrollTo(pos: Int) { + (binding.rvFind.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0) + } + + override fun openExplore(sourceUrl: String, title: String, exploreUrl: String?) { + if (exploreUrl.isNullOrBlank()) return + startActivity { + putExtra("exploreName", title) + putExtra("sourceUrl", sourceUrl) + putExtra("exploreUrl", exploreUrl) + } + } + + override fun editSource(sourceUrl: String) { + startActivity { + putExtra("sourceUrl", sourceUrl) + } + } + + override fun toTop(source: BookSourcePart) { + viewModel.topSource(source) + } + + override fun deleteSource(source: BookSourcePart) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + source.bookSourceName) + noButton() + yesButton { + viewModel.deleteSource(source) + } + } + } + + override fun searchBook(bookSource: BookSourcePart) { + startActivity { + putExtra("searchScope", SearchScope(bookSource).toString()) + } + } + + fun compressExplore() { + if (!adapter.compressExplore()) { + if (AppConfig.isEInkMode) { + binding.rvFind.scrollToPosition(0) + } else { + binding.rvFind.smoothScrollToPosition(0) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt new file mode 100755 index 000000000..f5df02093 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt @@ -0,0 +1,26 @@ +package io.legado.app.ui.main.explore + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.help.config.SourceConfig +import io.legado.app.help.source.SourceHelp + +class ExploreViewModel(application: Application) : BaseViewModel(application) { + + fun topSource(bookSource: BookSourcePart) { + execute { + val minXh = appDb.bookSourceDao.minOrder + bookSource.customOrder = minXh - 1 + appDb.bookSourceDao.upOrder(bookSource) + } + } + + fun deleteSource(source: BookSourcePart) { + execute { + SourceHelp.deleteBookSource(source.bookSourceUrl) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt new file mode 100755 index 000000000..e60e77441 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt @@ -0,0 +1,174 @@ +package io.legado.app.ui.main.my + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.preference.Preference +import io.legado.app.R +import io.legado.app.base.BaseFragment +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.databinding.FragmentMyConfigBinding +import io.legado.app.help.config.ThemeConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.prefs.NameListPreference +import io.legado.app.lib.prefs.SwitchPreference +import io.legado.app.lib.prefs.fragment.PreferenceFragment +import io.legado.app.lib.theme.primaryColor +import io.legado.app.service.WebService +import io.legado.app.ui.about.AboutActivity +import io.legado.app.ui.about.ReadRecordActivity +import io.legado.app.ui.book.bookmark.AllBookmarkActivity +import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity +import io.legado.app.ui.config.ConfigActivity +import io.legado.app.ui.config.ConfigTag +import io.legado.app.ui.dict.rule.DictRuleActivity +import io.legado.app.ui.file.FileManageActivity +import io.legado.app.ui.main.MainFragmentInterface +import io.legado.app.ui.replace.ReplaceRuleActivity +import io.legado.app.utils.LogUtils +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.observeEventSticky +import io.legado.app.utils.openUrl +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showHelp +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class MyFragment() : BaseFragment(R.layout.fragment_my_config), MainFragmentInterface { + + constructor(position: Int) : this() { + val bundle = Bundle() + bundle.putInt("position", position) + arguments = bundle + } + + override val position: Int? get() = arguments?.getInt("position") + + private val binding by viewBinding(FragmentMyConfigBinding::bind) + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + val fragmentTag = "prefFragment" + var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag) + if (preferenceFragment == null) preferenceFragment = MyPreferenceFragment() + childFragmentManager.beginTransaction() + .replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit() + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_my, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + when (item.itemId) { + R.id.menu_help -> showHelp("appHelp") + } + } + + /** + * 配置 + */ + class MyPreferenceFragment : PreferenceFragment(), + SharedPreferences.OnSharedPreferenceChangeListener { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + putPrefBoolean(PreferKey.webService, WebService.isRun) + addPreferencesFromResource(R.xml.pref_main) + findPreference("webService")?.onLongClick { + if (!WebService.isRun) { + return@onLongClick false + } + context?.selector(arrayListOf("复制地址", "浏览器打开")) { _, i -> + when (i) { + 0 -> context?.sendToClip(it.summary.toString()) + 1 -> context?.openUrl(it.summary.toString()) + } + } + true + } + observeEventSticky(EventBus.WEB_SERVICE) { + findPreference(PreferKey.webService)?.let { + it.isChecked = WebService.isRun + it.summary = if (WebService.isRun) { + WebService.hostAddress + } else { + getString(R.string.web_service_desc) + } + } + } + findPreference(PreferKey.themeMode)?.let { + it.setOnPreferenceChangeListener { _, _ -> + view?.post { ThemeConfig.applyDayNight(requireContext()) } + true + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listView.setEdgeEffectColor(primaryColor) + } + + override fun onResume() { + super.onResume() + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + super.onPause() + } + + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String? + ) { + when (key) { + PreferKey.webService -> { + if (requireContext().getPrefBoolean("webService")) { + WebService.start(requireContext()) + } else { + WebService.stop(requireContext()) + } + } + + "recordLog" -> LogUtils.upLevel() + } + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + "bookSourceManage" -> startActivity() + "replaceManage" -> startActivity() + "dictRuleManage" -> startActivity() + "txtTocRuleManage" -> startActivity() + "bookmark" -> startActivity() + "setting" -> startActivity { + putExtra("configTag", ConfigTag.OTHER_CONFIG) + } + + "web_dav_setting" -> startActivity { + putExtra("configTag", ConfigTag.BACKUP_CONFIG) + } + + "theme_setting" -> startActivity { + putExtra("configTag", ConfigTag.THEME_CONFIG) + } + + "fileManage" -> startActivity() + "readRecord" -> startActivity() + "about" -> startActivity() + "exit" -> activity?.finish() + } + return super.onPreferenceTreeClick(preference) + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt new file mode 100755 index 000000000..71b04d480 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt @@ -0,0 +1,86 @@ +package io.legado.app.ui.main.rss + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import com.bumptech.glide.request.RequestOptions +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ItemRssBinding +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import splitties.views.onLongClick + +class RssAdapter( + context: Context, + private val fragment: Fragment, + private val callBack: CallBack, + private val lifecycle: Lifecycle +) : RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemRssBinding { + return ItemRssBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssBinding, + item: RssSource, + payloads: MutableList + ) { + binding.apply { + tvName.text = item.sourceName + val options = RequestOptions() + .set(OkHttpModelLoader.sourceOriginOption, item.sourceUrl) + ImageLoader.load(fragment, lifecycle, item.sourceIcon) + .apply(options) + .centerCrop() + .placeholder(R.drawable.image_rss) + .error(R.drawable.image_rss) + .into(ivIcon) + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssBinding) { + binding.apply { + root.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + callBack.openRss(it) + } + } + root.onLongClick { + getItemByLayoutPosition(holder.layoutPosition)?.let { + showMenu(ivIcon, it) + } + } + } + } + + private fun showMenu(view: View, rssSource: RssSource) { + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.rss_main_item) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_top -> callBack.toTop(rssSource) + R.id.menu_edit -> callBack.edit(rssSource) + R.id.menu_del -> callBack.del(rssSource) + R.id.menu_disable -> callBack.disable(rssSource) + } + true + } + popupMenu.show() + } + + interface CallBack { + fun openRss(rssSource: RssSource) + fun toTop(rssSource: RssSource) + fun edit(rssSource: RssSource) + fun del(rssSource: RssSource) + fun disable(rssSource: RssSource) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt new file mode 100755 index 000000000..5bd401267 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -0,0 +1,222 @@ +package io.legado.app.ui.main.rss + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import android.view.View +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppLog +import io.legado.app.data.AppDatabase +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.FragmentRssBinding +import io.legado.app.databinding.ItemRssBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.main.MainFragmentInterface +import io.legado.app.ui.rss.article.RssSortActivity +import io.legado.app.ui.rss.favorites.RssFavoritesActivity +import io.legado.app.ui.rss.read.ReadRssActivity +import io.legado.app.ui.rss.source.edit.RssSourceEditActivity +import io.legado.app.ui.rss.source.manage.RssSourceActivity +import io.legado.app.ui.rss.subscription.RuleSubActivity +import io.legado.app.utils.applyTint +import io.legado.app.utils.flowWithLifecycleAndDatabaseChange +import io.legado.app.utils.openUrl +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.startActivity +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + + +/** + * 订阅界面 + */ +class RssFragment() : VMBaseFragment(R.layout.fragment_rss), + MainFragmentInterface, + RssAdapter.CallBack { + + constructor(position: Int) : this() { + val bundle = Bundle() + bundle.putInt("position", position) + arguments = bundle + } + + override val position: Int? get() = arguments?.getInt("position") + + private val binding by viewBinding(FragmentRssBinding::bind) + override val viewModel by viewModels() + private val adapter by lazy { + RssAdapter(requireContext(), this, this, viewLifecycleOwner.lifecycle) + } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var groupsFlowJob: Job? = null + private var rssFlowJob: Job? = null + private val groups = linkedSetOf() + private var groupsMenu: SubMenu? = null + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + setSupportToolbar(binding.titleBar.toolbar) + initSearchView() + initRecyclerView() + initGroupData() + upRssFlowJob() + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + menuInflater.inflate(R.menu.main_rss, menu) + groupsMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupsMenu() + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + when (item.itemId) { + R.id.menu_rss_config -> startActivity() + R.id.menu_rss_star -> startActivity() + else -> if (item.groupId == R.id.menu_group_text) { + searchView.setQuery("group:${item.title}", true) + } + } + } + + override fun onPause() { + super.onPause() + searchView.clearFocus() + } + + private fun upGroupsMenu() = groupsMenu?.transaction { subMenu -> + subMenu.removeGroup(R.id.menu_group_text) + groups.forEach { + subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it) + } + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.isSubmitButtonEnabled = true + searchView.queryHint = getString(R.string.rss) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + upRssFlowJob(newText) + return false + } + }) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.adapter = adapter + adapter.addHeaderView { + ItemRssBinding.inflate(layoutInflater, it, false).apply { + tvName.setText(R.string.rule_subscription) + ivIcon.setImageResource(R.drawable.image_legado) + root.setOnClickListener { + startActivity() + } + } + } + } + + private fun initGroupData() { + groupsFlowJob?.cancel() + groupsFlowJob = viewLifecycleOwner.lifecycleScope.launch { + appDb.rssSourceDao.flowEnabledGroups().catch { + AppLog.put("订阅界面获取分组数据失败\n${it.localizedMessage}", it) + }.flowWithLifecycleAndDatabaseChange( + viewLifecycleOwner.lifecycle, + Lifecycle.State.RESUMED, + AppDatabase.RSS_SOURCE_TABLE_NAME + ).conflate().collect { + groups.clear() + groups.addAll(it) + upGroupsMenu() + } + } + } + + private fun upRssFlowJob(searchKey: String? = null) { + rssFlowJob?.cancel() + rssFlowJob = viewLifecycleOwner.lifecycleScope.launch { + when { + searchKey.isNullOrEmpty() -> appDb.rssSourceDao.flowEnabled() + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.rssSourceDao.flowEnabledByGroup(key) + } + + else -> appDb.rssSourceDao.flowEnabled(searchKey) + }.flowWithLifecycleAndDatabaseChange( + viewLifecycleOwner.lifecycle, + Lifecycle.State.RESUMED, + AppDatabase.RSS_SOURCE_TABLE_NAME + ).catch { + AppLog.put("订阅界面更新数据出错", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + override fun openRss(rssSource: RssSource) { + if (rssSource.singleUrl) { + viewModel.getSingleUrl(rssSource) { url -> + if (url.startsWith("http", true)) { + startActivity { + putExtra("title", rssSource.sourceName) + putExtra("origin", url) + } + } else { + context?.openUrl(url) + } + } + } else { + startActivity { + putExtra("url", rssSource.sourceUrl) + } + } + } + + override fun toTop(rssSource: RssSource) { + viewModel.topSource(rssSource) + } + + override fun edit(rssSource: RssSource) { + startActivity { + putExtra("sourceUrl", rssSource.sourceUrl) + } + } + + override fun del(rssSource: RssSource) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + rssSource.sourceName) + noButton() + yesButton { + viewModel.del(rssSource) + } + } + } + + override fun disable(rssSource: RssSource) { + viewModel.disable(rssSource) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt new file mode 100755 index 000000000..ace716925 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssViewModel.kt @@ -0,0 +1,83 @@ +package io.legado.app.ui.main.rss + +import android.app.Application +import com.script.rhino.runScriptWithContext +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.toastOnUi + +class RssViewModel(application: Application) : BaseViewModel(application) { + + fun topSource(vararg sources: RssSource) { + execute { + sources.sortBy { it.customOrder } + val minOrder = appDb.rssSourceDao.minOrder - 1 + val array = Array(sources.size) { + sources[it].copy(customOrder = minOrder - it) + } + appDb.rssSourceDao.update(*array) + } + } + + fun bottomSource(vararg sources: RssSource) { + execute { + sources.sortBy { it.customOrder } + val maxOrder = appDb.rssSourceDao.maxOrder + 1 + val array = Array(sources.size) { + sources[it].copy(customOrder = maxOrder + it) + } + appDb.rssSourceDao.update(*array) + } + } + + fun del(vararg rssSource: RssSource) { + execute { + SourceHelp.deleteRssSources(rssSource.toList()) + } + } + + fun disable(rssSource: RssSource) { + execute { + rssSource.enabled = false + appDb.rssSourceDao.update(rssSource) + } + } + + fun getSingleUrl(rssSource: RssSource, onSuccess: (url: String) -> Unit) { + execute { + var sortUrl = rssSource.sortUrl + if (!sortUrl.isNullOrBlank()) { + if (sortUrl.startsWith("", false) + || sortUrl.startsWith("@js:", false) + ) { + val jsStr = if (sortUrl.startsWith("@")) { + sortUrl.substring(4) + } else { + sortUrl.substring(4, sortUrl.lastIndexOf("<")) + } + val result = runScriptWithContext { + rssSource.evalJS(jsStr)?.toString() + } + if (!result.isNullOrBlank()) { + sortUrl = result + } + } + if (sortUrl.contains("::")) { + return@execute sortUrl.split("::")[1] + } else { + return@execute sortUrl + } + } + rssSource.sourceUrl + }.timeout(10000) + .onSuccess { + onSuccess.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt new file mode 100755 index 000000000..a67456dc0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt @@ -0,0 +1,57 @@ +package io.legado.app.ui.qrcode + +import android.content.Intent +import android.graphics.BitmapFactory +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import com.google.zxing.Result +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.databinding.ActivityQrcodeCaptureBinding +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.utils.QRCodeUtils +import io.legado.app.utils.readBytes +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class QrCodeActivity : BaseActivity(), ScanResultCallback { + + override val binding by viewBinding(ActivityQrcodeCaptureBinding::inflate) + + private val selectQrImage = registerForActivityResult(HandleFileContract()) { + it.uri?.readBytes(this)?.let { bytes -> + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + onScanResultCallback(QRCodeUtils.parseCodeResult(bitmap)) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + val fTag = "qrCodeFragment" + val qrCodeFragment = QrCodeFragment() + supportFragmentManager.beginTransaction() + .replace(R.id.fl_content, qrCodeFragment, fTag) + .commit() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.qr_code_scan, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_choose_from_gallery -> selectQrImage.launch { + mode = HandleFileContract.IMAGE + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onScanResultCallback(result: Result?) { + val intent = Intent() + intent.putExtra("result", result?.text) + setResult(RESULT_OK, intent) + finish() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt new file mode 100755 index 000000000..8f661d5d9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeFragment.kt @@ -0,0 +1,33 @@ +package io.legado.app.ui.qrcode + +import com.google.zxing.Result +import com.king.camera.scan.AnalyzeResult +import com.king.camera.scan.CameraScan +import com.king.zxing.BarcodeCameraScanFragment +import com.king.zxing.DecodeConfig +import com.king.zxing.DecodeFormatManager +import com.king.zxing.analyze.MultiFormatAnalyzer + +class QrCodeFragment : BarcodeCameraScanFragment() { + + override fun initCameraScan(cameraScan: CameraScan) { + super.initCameraScan(cameraScan) + //初始化解码配置 + val decodeConfig = DecodeConfig() + //如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS + decodeConfig.hints = DecodeFormatManager.QR_CODE_HINTS + //设置是否全区域识别,默认false + decodeConfig.isFullAreaScan = true + //设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别 + decodeConfig.areaRectRatio = 0.8f + + //在启动预览之前,设置分析器,只识别二维码 + cameraScan.setAnalyzer(MultiFormatAnalyzer(decodeConfig)) + } + + override fun onScanResultCallback(result: AnalyzeResult) { + cameraScan.setAnalyzeImage(false) + (activity as? QrCodeActivity)?.onScanResultCallback(result.result) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt new file mode 100755 index 000000000..d93184ca3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeResult.kt @@ -0,0 +1,23 @@ +package io.legado.app.ui.qrcode + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract + +class QrCodeResult : ActivityResultContract() { + + override fun createIntent(context: Context, input: Unit?): Intent { + return Intent(context, QrCodeActivity::class.java) + } + + override fun parseResult(resultCode: Int, intent: Intent?): String? { + if (resultCode == RESULT_OK) { + intent?.getStringExtra("result")?.let { + return it + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/qrcode/ScanResultCallback.kt b/app/src/main/java/io/legado/app/ui/qrcode/ScanResultCallback.kt new file mode 100755 index 000000000..9cb61d58e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/qrcode/ScanResultCallback.kt @@ -0,0 +1,9 @@ +package io.legado.app.ui.qrcode + +import com.google.zxing.Result + +interface ScanResultCallback { + + fun onScanResultCallback(result: Result?) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt new file mode 100755 index 000000000..4b7a0831f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/GroupManageDialog.kt @@ -0,0 +1,144 @@ +package io.legado.app.ui.replace + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemGroupManageBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.requestInputMethod +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch + + +class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val viewModel: ReplaceRuleViewModel by activityViewModels() + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { GroupAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + view.setBackgroundColor(backgroundColor) + binding.toolBar.setBackgroundColor(primaryColor) + initView() + initData() + } + + private fun initView() = binding.run { + toolBar.title = getString(R.string.group_manage) + toolBar.inflateMenu(R.menu.group_manage) + toolBar.menu.applyTint(requireContext()) + toolBar.setOnMenuItemClickListener(this@GroupManageDialog) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + } + + private fun initData() { + lifecycleScope.launch { + appDb.replaceRuleDao.flowGroups().conflate().collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> addGroup() + } + return true + } + + @SuppressLint("InflateParams") + private fun addGroup() { + alert(title = getString(R.string.add_group)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotBlank()) { + viewModel.addGroup(it) + } + } + } + cancelButton() + }.requestInputMethod() + } + + @SuppressLint("InflateParams") + private fun editGroup(group: String) { + alert(title = getString(R.string.group_edit)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setText(group) + } + customView { alertBinding.root } + okButton { + viewModel.upGroup(group, alertBinding.editView.text?.toString()) + } + cancelButton() + }.requestInputMethod() + } + + private inner class GroupAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding { + return ItemGroupManageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupManageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) { + binding.apply { + tvEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + + tvDel.setOnClickListener { + getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt new file mode 100755 index 000000000..9e40a580e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt @@ -0,0 +1,361 @@ +package io.legado.app.ui.replace + +import android.annotation.SuppressLint +import android.app.Activity +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.ActivityReplaceRuleBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.association.ImportReplaceRuleDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.replace.edit.ReplaceEditActivity +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ACache +import io.legado.app.utils.GSON +import io.legado.app.utils.applyTint +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.launch +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 替换规则管理 + */ +class ReplaceRuleActivity : VMBaseActivity(), + SearchView.OnQueryTextListener, + PopupMenu.OnMenuItemClickListener, + SelectActionBar.CallBack, + ReplaceRuleAdapter.CallBack { + override val binding by viewBinding(ActivityReplaceRuleBinding::inflate) + override val viewModel by viewModels() + private val importRecordKey = "replaceRuleRecordKey" + private val adapter by lazy { ReplaceRuleAdapter(this, this) } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var groups = arrayListOf() + private var groupMenu: SubMenu? = null + private var replaceRuleFlowJob: Job? = null + private var dataInit = false + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + showDialogFragment(ImportReplaceRuleDialog(it)) + } + private val editActivity = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + setResult(RESULT_OK) + } + } + private val importDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportReplaceRuleDialog(uri.toString())) + } + } + private val exportResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + sendToClip(uri.toString()) + } + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initRecyclerView() + initSearchView() + initSelectActionView() + observeReplaceRuleData() + observeGroupData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.replace_rule, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + groupMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupMenu() + return super.onPrepareOptionsMenu(menu) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + // When this page is opened, it is in selection mode + dragSelectTouchHelper.activeSlideSelect() + + // Note: need judge selection first, so add ItemTouchHelper after it. + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initSearchView() { + searchView.applyTint(primaryTextColor) + searchView.queryHint = getString(R.string.replace_purify_search) + searchView.setOnQueryTextListener(this) + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickSelectBarMainAction() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + yesButton { viewModel.delSelection(adapter.selection) } + noButton() + } + } + + private fun initSelectActionView() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.replace_rule_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun observeReplaceRuleData(searchKey: String? = null) { + dataInit = false + replaceRuleFlowJob?.cancel() + replaceRuleFlowJob = lifecycleScope.launch { + when { + searchKey.isNullOrEmpty() -> { + appDb.replaceRuleDao.flowAll() + } + + searchKey == getString(R.string.no_group) -> { + appDb.replaceRuleDao.flowNoGroup() + } + + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.replaceRuleDao.flowGroupSearch("%$key%") + } + + else -> { + appDb.replaceRuleDao.flowSearch("%$searchKey%") + } + }.catch { + AppLog.put("替换规则管理界面更新数据出错", it) + }.flowOn(IO).conflate().collect { + if (dataInit) { + setResult(Activity.RESULT_OK) + } + adapter.setItems(it, adapter.diffItemCallBack) + dataInit = true + delay(100) + } + } + } + + private fun observeGroupData() { + lifecycleScope.launch { + appDb.replaceRuleDao.flowGroups().collect { + groups.clear() + groups.addAll(it) + upGroupMenu() + } + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add_replace_rule -> + editActivity.launch(ReplaceEditActivity.startIntent(this)) + + R.id.menu_group_manage -> showDialogFragment() + R.id.menu_del_selection -> viewModel.delSelection(adapter.selection) + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_local -> importDoc.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_qr -> qrCodeResult.launch() + R.id.menu_help -> showHelp("replaceRuleHelp") + R.id.menu_group_null -> { + searchView.setQuery(getString(R.string.no_group), true) + } + + else -> if (item.groupId == R.id.replace_group) { + searchView.setQuery("group:${item.title}", true) + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection) + R.id.menu_top_sel -> viewModel.topSelect(adapter.selection) + R.id.menu_bottom_sel -> viewModel.bottomSelect(adapter.selection) + R.id.menu_export_selection -> exportResult.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "exportReplaceRule.json", + GSON.toJson(adapter.selection).toByteArray(), + "application/json" + ) + } + } + return false + } + + private fun upGroupMenu() = groupMenu?.transaction { menu -> + menu.removeGroup(R.id.replace_group) + groups.forEach { + menu.add(R.id.replace_group, Menu.NONE, Menu.NONE, it) + } + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(importRecordKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (it.isAbsUrl() && !cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + showDialogFragment( + ImportReplaceRuleDialog(it) + ) + } + } + cancelButton() + } + } + + override fun onQueryTextChange(newText: String?): Boolean { + observeReplaceRuleData(newText) + return false + } + + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onDestroy() { + super.onDestroy() + Coroutine.async { ContentProcessor.upReplaceRules() } + } + + override fun upCountView() { + binding.selectActionBar.upCountView( + adapter.selection.size, + adapter.itemCount + ) + } + + override fun update(vararg rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.update(*rule) + } + + override fun delete(rule: ReplaceRule) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + rule.name) + noButton() + yesButton { + setResult(RESULT_OK) + viewModel.delete(rule) + } + } + } + + override fun edit(rule: ReplaceRule) { + setResult(RESULT_OK) + editActivity.launch(ReplaceEditActivity.startIntent(this, rule.id)) + } + + override fun toTop(rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.toTop(rule) + } + + override fun toBottom(rule: ReplaceRule) { + setResult(RESULT_OK) + viewModel.toBottom(rule) + } + + override fun upOrder() { + setResult(RESULT_OK) + viewModel.upOrder() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt new file mode 100755 index 000000000..03f7eb7c0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleAdapter.kt @@ -0,0 +1,234 @@ +package io.legado.app.ui.replace + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.ItemReplaceRuleBinding +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.ColorUtils + + +class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private val selected = linkedSetOf() + + val selection: List + get() { + return getItems().filter { + selected.contains(it) + } + } + + val diffItemCallBack = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean { + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.group != newItem.group) { + return false + } + if (oldItem.isEnabled != newItem.isEnabled) { + return false + } + return true + } + + override fun getChangePayload(oldItem: ReplaceRule, newItem: ReplaceRule): Any? { + val payload = Bundle() + if (oldItem.name != newItem.name + || oldItem.group != newItem.group + ) { + payload.putBoolean("upName", true) + } + if (oldItem.isEnabled != newItem.isEnabled) { + payload.putBoolean("enabled", newItem.isEnabled) + } + if (payload.isEmpty) { + return null + } + return payload + } + } + + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + override fun getViewBinding(parent: ViewGroup): ItemReplaceRuleBinding { + return ItemReplaceRuleBinding.inflate(inflater, parent, false) + } + + override fun onCurrentListChanged() { + callBack.upCountView() + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemReplaceRuleBinding, + item: ReplaceRule, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) + cbName.text = item.getDisplayNameGroup() + swtEnabled.isChecked = item.isEnabled + cbName.isChecked = selected.contains(item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "selected" -> cbName.isChecked = selected.contains(item) + "upName" -> cbName.text = item.getDisplayNameGroup() + "enabled" -> swtEnabled.isChecked = item.isEnabled + } + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemReplaceRuleBinding) { + binding.apply { + swtEnabled.setOnUserCheckedChangeListener { isChecked -> + getItem(holder.layoutPosition)?.let { + it.isEnabled = isChecked + callBack.update(it) + } + } + ivEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) + } + } + cbName.setOnClickListener { + getItem(holder.layoutPosition)?.let { + if (cbName.isChecked) { + selected.add(it) + } else { + selected.remove(it) + } + } + callBack.upCountView() + } + ivMenuMore.setOnClickListener { + showMenu(ivMenuMore, holder.layoutPosition) + } + } + } + + private fun showMenu(view: View, position: Int) { + val item = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.replace_rule_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(item) + R.id.menu_bottom -> callBack.toBottom(item) + R.id.menu_del -> { + callBack.delete(item) + selected.remove(item) + } + } + true + } + popupMenu.show() + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.order == targetItem.order) { + callBack.upOrder() + } else { + val srcOrder = srcItem.order + srcItem.order = targetItem.order + targetItem.order = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = linkedSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() + } + } + + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selected + } + + override fun getItemId(position: Int): ReplaceRule { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upCountView() + return true + } + return false + } + } + + interface CallBack { + fun update(vararg rule: ReplaceRule) + fun delete(rule: ReplaceRule) + fun edit(rule: ReplaceRule) + fun toTop(rule: ReplaceRule) + fun toBottom(rule: ReplaceRule) + fun upOrder() + fun upCountView() + } +} diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt new file mode 100755 index 000000000..f504c8905 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt @@ -0,0 +1,135 @@ +package io.legado.app.ui.replace + +import android.app.Application +import android.text.TextUtils +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.utils.splitNotBlank + +/** + * 替换规则数据修改 + * 修改数据要copy,直接修改会导致界面不刷新 + */ +class ReplaceRuleViewModel(application: Application) : BaseViewModel(application) { + + fun update(vararg rule: ReplaceRule) { + execute { + appDb.replaceRuleDao.update(*rule) + } + } + + fun delete(rule: ReplaceRule) { + execute { + appDb.replaceRuleDao.delete(rule) + } + } + + fun toTop(rule: ReplaceRule) { + execute { + rule.order = appDb.replaceRuleDao.minOrder - 1 + appDb.replaceRuleDao.update(rule) + } + } + + fun topSelect(rules: List) { + execute { + var minOrder = appDb.replaceRuleDao.minOrder - rules.size + rules.forEach { + it.order = ++minOrder + } + appDb.replaceRuleDao.update(*rules.toTypedArray()) + } + } + + fun toBottom(rule: ReplaceRule) { + execute { + rule.order = appDb.replaceRuleDao.maxOrder + 1 + appDb.replaceRuleDao.update(rule) + } + } + + fun bottomSelect(rules: List) { + execute { + var maxOrder = appDb.replaceRuleDao.maxOrder + rules.forEach { + it.order = maxOrder++ + } + appDb.replaceRuleDao.update(*rules.toTypedArray()) + } + } + + fun upOrder() { + execute { + val rules = appDb.replaceRuleDao.all + for ((index, rule) in rules.withIndex()) { + rule.order = index + 1 + } + appDb.replaceRuleDao.update(*rules.toTypedArray()) + } + } + + fun enableSelection(rules: List) { + execute { + val array = Array(rules.size) { + rules[it].copy(isEnabled = true) + } + appDb.replaceRuleDao.update(*array) + } + } + + fun disableSelection(rules: List) { + execute { + val array = Array(rules.size) { + rules[it].copy(isEnabled = false) + } + appDb.replaceRuleDao.update(*array) + } + } + + fun delSelection(rules: List) { + execute { + appDb.replaceRuleDao.delete(*rules.toTypedArray()) + } + } + + fun addGroup(group: String) { + execute { + val sources = appDb.replaceRuleDao.noGroup + sources.forEach { source -> + source.group = group + } + appDb.replaceRuleDao.update(*sources.toTypedArray()) + } + } + + fun upGroup(oldGroup: String, newGroup: String?) { + execute { + val sources = appDb.replaceRuleDao.getByGroup(oldGroup) + sources.forEach { source -> + source.group?.splitNotBlank(",")?.toHashSet()?.let { + it.remove(oldGroup) + if (!newGroup.isNullOrEmpty()) + it.add(newGroup) + source.group = TextUtils.join(",", it) + } + } + appDb.replaceRuleDao.update(*sources.toTypedArray()) + } + } + + fun delGroup(group: String) { + execute { + execute { + val sources = appDb.replaceRuleDao.getByGroup(group) + sources.forEach { source -> + source.group?.splitNotBlank(",")?.toHashSet()?.let { + it.remove(group) + source.group = TextUtils.join(",", it) + } + } + appDb.replaceRuleDao.update(*sources.toTypedArray()) + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt new file mode 100755 index 000000000..34a216b9a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt @@ -0,0 +1,159 @@ +package io.legado.app.ui.replace.edit + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.EditText +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.databinding.ActivityReplaceEditBinding +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.ui.widget.keyboard.KeyboardToolPop +import io.legado.app.utils.GSON +import io.legado.app.utils.imeHeight +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.showHelp +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 编辑替换规则 + */ +class ReplaceEditActivity : + VMBaseActivity(), + KeyboardToolPop.CallBack { + + companion object { + + fun startIntent( + context: Context, + id: Long = -1, + pattern: String? = null, + isRegex: Boolean = false, + scope: String? = null + ): Intent { + val intent = Intent(context, ReplaceEditActivity::class.java) + intent.putExtra("id", id) + intent.putExtra("pattern", pattern) + intent.putExtra("isRegex", isRegex) + intent.putExtra("scope", scope) + return intent + } + + } + + override val binding by viewBinding(ActivityReplaceEditBinding::inflate) + override val viewModel by viewModels() + + private val softKeyboardTool by lazy { + KeyboardToolPop(this, lifecycleScope, binding.root, this) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + softKeyboardTool.attachToWindow(window) + initView() + viewModel.initData(intent) { + upReplaceView(it) + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.replace_edit, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> viewModel.save(getReplaceRule()) { + setResult(RESULT_OK) + finish() + } + + R.id.menu_copy_rule -> sendToClip(GSON.toJson(getReplaceRule())) + R.id.menu_paste_rule -> viewModel.pasteRule { + upReplaceView(it) + } + } + return true + } + + override fun onDestroy() { + super.onDestroy() + softKeyboardTool.dismiss() + } + + private fun initView() { + binding.ivHelp.setOnClickListener { + showHelp("regexHelp") + } + binding.root.setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + softKeyboardTool.initialPadding = windowInsets.imeHeight + windowInsets + } + } + + private fun upReplaceView(replaceRule: ReplaceRule) = binding.run { + etName.setText(replaceRule.name) + etGroup.setText(replaceRule.group) + etReplaceRule.setText(replaceRule.pattern) + cbUseRegex.isChecked = replaceRule.isRegex + etReplaceTo.setText(replaceRule.replacement) + cbScopeTitle.isChecked = replaceRule.scopeTitle + cbScopeContent.isChecked = replaceRule.scopeContent + etScope.setText(replaceRule.scope) + etExcludeScope.setText(replaceRule.excludeScope) + etTimeout.setText(replaceRule.timeoutMillisecond.toString()) + } + + private fun getReplaceRule(): ReplaceRule = binding.run { + val replaceRule: ReplaceRule = viewModel.replaceRule ?: ReplaceRule() + replaceRule.name = etName.text.toString() + replaceRule.group = etGroup.text.toString() + replaceRule.pattern = etReplaceRule.text.toString() + replaceRule.isRegex = cbUseRegex.isChecked + replaceRule.replacement = etReplaceTo.text.toString() + replaceRule.scopeTitle = cbScopeTitle.isChecked + replaceRule.scopeContent = cbScopeContent.isChecked + replaceRule.scope = etScope.text.toString() + replaceRule.excludeScope = etExcludeScope.text.toString() + replaceRule.timeoutMillisecond = etTimeout.text.toString().ifEmpty { "3000" }.toLong() + return replaceRule + } + + override fun helpActions(): List> { + return arrayListOf( + SelectItem("正则教程", "regexHelp") + ) + } + + override fun onHelpActionSelect(action: String) { + when (action) { + "regexHelp" -> showHelp("regexHelp") + } + } + + override fun sendText(text: String) { + if (text.isBlank()) return + val view = window?.decorView?.findFocus() + if (view is EditText) { + val start = view.selectionStart + val end = view.selectionEnd + //获取EditText的文字 + val edit = view.editableText + if (start < 0 || start >= edit.length) { + edit.append(text) + } else if (start > end) { + edit.replace(end, start, text) + } else { + //光标所在位置插入文字 + edit.replace(start, end, text) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt new file mode 100755 index 000000000..36dc19f1d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt @@ -0,0 +1,69 @@ +package io.legado.app.ui.replace.edit + +import android.app.Application +import android.content.Intent +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.exception.NoStackTraceException +import io.legado.app.utils.* +import kotlinx.coroutines.Dispatchers + +class ReplaceEditViewModel(application: Application) : BaseViewModel(application) { + + var replaceRule: ReplaceRule? = null + + fun initData(intent: Intent, finally: (replaceRule: ReplaceRule) -> Unit) { + execute { + val id = intent.getLongExtra("id", -1) + replaceRule = if (id > 0) { + appDb.replaceRuleDao.findById(id) + } else { + val pattern = intent.getStringExtra("pattern") ?: "" + val isRegex = intent.getBooleanExtra("isRegex", false) + val scope = intent.getStringExtra("scope") + ReplaceRule( + name = pattern, + pattern = pattern, + isRegex = isRegex, + scope = scope + ) + } + }.onFinally { + replaceRule?.let { + finally(it) + } + } + } + + fun pasteRule(success: (ReplaceRule) -> Unit) { + execute(context = Dispatchers.Main) { + val text = context.getClipText() + if (text.isNullOrBlank()) { + throw NoStackTraceException("剪贴板为空") + } + GSON.fromJsonObject(text).getOrNull() + ?: throw NoStackTraceException("格式不对") + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.localizedMessage ?: "Error") + it.printOnDebug() + } + } + + fun save(replaceRule: ReplaceRule, success: () -> Unit) { + execute { + replaceRule.checkValid() + if (replaceRule.order == Int.MIN_VALUE) { + replaceRule.order = appDb.replaceRuleDao.maxOrder + 1 + } + appDb.replaceRuleDao.insert(replaceRule) + }.onSuccess { + success() + }.onError { + context.toastOnUi("save error, ${it.localizedMessage}") + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt new file mode 100755 index 000000000..69eb4ccd6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/BaseRssArticlesAdapter.kt @@ -0,0 +1,16 @@ +package io.legado.app.ui.rss.article + +import android.content.Context +import androidx.viewbinding.ViewBinding +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RssArticle + + +abstract class BaseRssArticlesAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + + interface CallBack { + val isGridLayout: Boolean + fun readRss(rssArticle: RssArticle) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/ReadRecordDialog.kt b/app/src/main/java/io/legado/app/ui/rss/article/ReadRecordDialog.kt new file mode 100755 index 000000000..53c29c94d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/ReadRecordDialog.kt @@ -0,0 +1,88 @@ +package io.legado.app.ui.rss.article + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RssReadRecord +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemRssReadRecordBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class ReadRecordDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val viewModel by viewModels() + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { + ReadRecordAdapter(requireContext()) + } + + override fun onStart() { + super.onStart() + setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.setTitle(R.string.read_record) + toolBar.inflateMenu(R.menu.rss_read_record) + toolBar.setOnMenuItemClickListener(this@ReadRecordDialog) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter + } + adapter.setItems(viewModel.getRecords()) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_clear -> { + alert(R.string.draw) { + val countRead = viewModel.countRecords() + setMessage(getString(R.string.sure_del) + "\n" + countRead + " " + getString(R.string.read_record)) + noButton() + yesButton { + viewModel.deleteAllRecord() + adapter.clearItems() + } + } + } + } + return true + } + + inner class ReadRecordAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemRssReadRecordBinding { + return ItemRssReadRecordBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssReadRecordBinding, + item: RssReadRecord, + payloads: MutableList + ) { + binding.textTitle.text = item.title + binding.textRecord.text = item.record + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssReadRecordBinding) { + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt new file mode 100755 index 000000000..aea7b3a5a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt @@ -0,0 +1,91 @@ +package io.legado.app.ui.rss.article + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.ItemRssArticleBinding +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class RssArticlesAdapter(context: Context, callBack: CallBack) : + BaseRssArticlesAdapter(context, callBack) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticleBinding { + return ItemRssArticleBinding.inflate(inflater, parent, false) + } + + @SuppressLint("CheckResult") + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticleBinding, + item: RssArticle, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate + if (item.image.isNullOrBlank() && !callBack.isGridLayout) { + imageView.gone() + } else { + val options = + RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin) + ImageLoader.load(context, item.image).apply(options).apply { + if (callBack.isGridLayout) { + placeholder(R.drawable.image_rss_article) + } else { + addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + imageView.gone() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + imageView.visible() + return false + } + + }) + } + }.into(imageView) + } + if (item.read) { + tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary)) + } else { + tvTitle.setTextColor(context.getCompatColor(R.color.primaryText)) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticleBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt new file mode 100755 index 000000000..0f7cbdb57 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter1.kt @@ -0,0 +1,91 @@ +package io.legado.app.ui.rss.article + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.ItemRssArticle1Binding +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class RssArticlesAdapter1(context: Context, callBack: CallBack) : + BaseRssArticlesAdapter(context, callBack) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticle1Binding { + return ItemRssArticle1Binding.inflate(inflater, parent, false) + } + + @SuppressLint("CheckResult") + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticle1Binding, + item: RssArticle, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate + if (item.image.isNullOrBlank() && !callBack.isGridLayout) { + imageView.gone() + } else { + val options = + RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin) + ImageLoader.load(context, item.image).apply(options).apply { + if (callBack.isGridLayout) { + placeholder(R.drawable.image_rss_article) + } else { + addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + imageView.gone() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + imageView.visible() + return false + } + + }) + } + }.into(imageView) + } + if (item.read) { + tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary)) + } else { + tvTitle.setTextColor(context.getCompatColor(R.color.primaryText)) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticle1Binding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt new file mode 100755 index 000000000..403d435be --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter2.kt @@ -0,0 +1,91 @@ +package io.legado.app.ui.rss.article + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.ItemRssArticle2Binding +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class RssArticlesAdapter2(context: Context, callBack: CallBack) : + BaseRssArticlesAdapter(context, callBack) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticle2Binding { + return ItemRssArticle2Binding.inflate(inflater, parent, false) + } + + @SuppressLint("CheckResult") + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticle2Binding, + item: RssArticle, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate + if (item.image.isNullOrBlank() && !callBack.isGridLayout) { + imageView.gone() + } else { + val options = + RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin) + ImageLoader.load(context, item.image).apply(options).apply { + if (callBack.isGridLayout) { + placeholder(R.drawable.image_rss_article) + } else { + addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + imageView.gone() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + imageView.visible() + return false + } + + }) + } + }.into(imageView) + } + if (item.read) { + tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary)) + } else { + tvTitle.setTextColor(context.getCompatColor(R.color.primaryText)) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticle2Binding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt new file mode 100755 index 000000000..652694273 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt @@ -0,0 +1,158 @@ +package io.legado.app.ui.rss.article + + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.FragmentRssArticlesBinding +import io.legado.app.databinding.ViewLoadMoreBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.rss.read.ReadRssActivity +import io.legado.app.ui.widget.recycler.LoadMoreView +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +class RssArticlesFragment() : VMBaseFragment(R.layout.fragment_rss_articles), + BaseRssArticlesAdapter.CallBack { + + constructor(sortName: String, sortUrl: String) : this() { + arguments = Bundle().apply { + putString("sortName", sortName) + putString("sortUrl", sortUrl) + } + } + + private val binding by viewBinding(FragmentRssArticlesBinding::bind) + private val activityViewModel by activityViewModels() + override val viewModel by viewModels() + private val adapter: BaseRssArticlesAdapter<*> by lazy { + when (activityViewModel.rssSource?.articleStyle) { + 1 -> RssArticlesAdapter1(requireContext(), this@RssArticlesFragment) + 2 -> RssArticlesAdapter2(requireContext(), this@RssArticlesFragment) + else -> RssArticlesAdapter(requireContext(), this@RssArticlesFragment) + } + } + private val loadMoreView: LoadMoreView by lazy { + LoadMoreView(requireContext()) + } + private var articlesFlowJob: Job? = null + override val isGridLayout: Boolean + get() = activityViewModel.isGridLayout + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.init(arguments) + initView() + initData() + } + + private fun initView() = binding.run { + refreshLayout.setColorSchemeColors(accentColor) + recyclerView.setEdgeEffectColor(primaryColor) + recyclerView.applyNavigationBarPadding() + loadMoreView.setOnClickListener { + if (!loadMoreView.isLoading) { + scrollToBottom(true) + } + } + recyclerView.layoutManager = if (activityViewModel.isGridLayout) { + recyclerView.setPadding(8, 0, 8, 0) + GridLayoutManager(requireContext(), 2) + } else { + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + LinearLayoutManager(requireContext()) + } + recyclerView.adapter = adapter + adapter.addFooterView { + ViewLoadMoreBinding.bind(loadMoreView) + } + refreshLayout.setOnRefreshListener { + loadArticles() + } + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (!recyclerView.canScrollVertically(1)) { + scrollToBottom() + } + } + }) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + refreshLayout.isRefreshing = true + loadArticles() + this@launch.cancel() + } + } + } + + private fun initData() { + val rssUrl = activityViewModel.url ?: return + articlesFlowJob?.cancel() + articlesFlowJob = viewLifecycleOwner.lifecycleScope.launch { + appDb.rssArticleDao.flowByOriginSort(rssUrl, viewModel.sortName).catch { + AppLog.put("订阅文章界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + private fun loadArticles() { + activityViewModel.rssSource?.let { + viewModel.loadArticles(it) + } + } + + private fun scrollToBottom(forceLoad: Boolean = false) { + if (viewModel.isLoading) return + if ((loadMoreView.hasMore && adapter.getActualItemCount() > 0) || forceLoad) { + loadMoreView.hasMore() + activityViewModel.rssSource?.let { + viewModel.loadMore(it) + } + } + } + + override fun observeLiveBus() { + viewModel.loadErrorLiveData.observe(viewLifecycleOwner) { + loadMoreView.error(it) + } + viewModel.loadFinallyLiveData.observe(viewLifecycleOwner) { hasMore -> + binding.refreshLayout.isRefreshing = false + if (!hasMore) { + loadMoreView.noMore() + } + } + } + + override fun readRss(rssArticle: RssArticle) { + activityViewModel.read(rssArticle) + startActivity { + putExtra("title", rssArticle.title) + putExtra("origin", rssArticle.origin) + putExtra("link", rssArticle.link) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt new file mode 100755 index 000000000..18b39c8ba --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt @@ -0,0 +1,96 @@ +package io.legado.app.ui.rss.article + +import android.app.Application +import android.os.Bundle +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssSource +import io.legado.app.model.rss.Rss +import io.legado.app.utils.stackTraceStr +import kotlinx.coroutines.Dispatchers.IO + + +class RssArticlesViewModel(application: Application) : BaseViewModel(application) { + val loadFinallyLiveData = MutableLiveData() + val loadErrorLiveData = MutableLiveData() + var isLoading = true + var order = System.currentTimeMillis() + private var nextPageUrl: String? = null + var sortName: String = "" + var sortUrl: String = "" + var page = 1 + + fun init(bundle: Bundle?) { + bundle?.let { + sortName = it.getString("sortName") ?: "" + sortUrl = it.getString("sortUrl") ?: "" + } + } + + fun loadArticles(rssSource: RssSource) { + isLoading = true + page = 1 + order = System.currentTimeMillis() + Rss.getArticles(viewModelScope, sortName, sortUrl, rssSource, page).onSuccess(IO) { + nextPageUrl = it.second + val articles = it.first + articles.forEach { rssArticle -> + rssArticle.order = order-- + } + appDb.rssArticleDao.insert(*articles.toTypedArray()) + if (!rssSource.ruleNextPage.isNullOrEmpty()) { + appDb.rssArticleDao.clearOld(rssSource.sourceUrl, sortName, order) + } + val hasMore = articles.isNotEmpty() && !rssSource.ruleNextPage.isNullOrEmpty() + loadFinallyLiveData.postValue(hasMore) + isLoading = false + }.onError { + loadFinallyLiveData.postValue(false) + AppLog.put("rss获取内容失败", it) + loadErrorLiveData.postValue(it.stackTraceStr) + } + } + + fun loadMore(rssSource: RssSource) { + isLoading = true + page++ + val pageUrl = nextPageUrl + if (pageUrl.isNullOrEmpty()) { + loadFinallyLiveData.postValue(false) + return + } + Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page).onSuccess(IO) { + nextPageUrl = it.second + loadMoreSuccess(it.first) + isLoading = false + }.onError { + loadFinallyLiveData.postValue(false) + AppLog.put("rss获取内容失败", it) + loadErrorLiveData.postValue(it.stackTraceStr) + } + } + + private fun loadMoreSuccess(articles: MutableList) { + if (articles.isEmpty()) { + loadFinallyLiveData.postValue(false) + return + } + val firstArticle = articles.first() + val dbFirstArticle = appDb.rssArticleDao.get(firstArticle.origin, firstArticle.link) + val lastArticle = articles.last() + val dbLastArticle = appDb.rssArticleDao.get(lastArticle.origin, lastArticle.link) + if (dbFirstArticle != null && dbLastArticle != null) { + loadFinallyLiveData.postValue(false) + } else { + articles.forEach { + it.order = order-- + } + appDb.rssArticleDao.append(*articles.toTypedArray()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt new file mode 100755 index 000000000..3f6b7009b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt @@ -0,0 +1,168 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.ui.rss.article + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.ViewGroup +import androidx.activity.viewModels +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivityRssArtivlesBinding +import io.legado.app.help.source.sortUrls +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.rss.source.edit.RssSourceEditActivity +import io.legado.app.ui.widget.dialog.VariableDialog +import io.legado.app.utils.* +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class RssSortActivity : VMBaseActivity(), + VariableDialog.Callback { + + override val binding by viewBinding(ActivityRssArtivlesBinding::inflate) + override val viewModel by viewModels() + private val adapter by lazy { TabFragmentPageAdapter() } + private val sortList = mutableListOf>() + private val fragmentMap = hashMapOf() + private val editSourceResult = registerForActivityResult( + StartActivityContract(RssSourceEditActivity::class.java) + ) { + if (it.resultCode == RESULT_OK) { + viewModel.initData(intent) { + upFragments() + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + binding.tabLayout.setSelectedTabIndicatorColor(accentColor) + viewModel.titleLiveData.observe(this) { + binding.titleBar.title = it + } + viewModel.initData(intent) { + upFragments() + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_articles, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_login)?.isVisible = + !viewModel.rssSource?.loginUrl.isNullOrBlank() + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_login -> startActivity { + putExtra("type", "rssSource") + putExtra("key", viewModel.rssSource?.sourceUrl) + } + + R.id.menu_refresh_sort -> viewModel.clearSortCache { upFragments() } + R.id.menu_set_source_variable -> setSourceVariable() + R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let { + editSourceResult.launch { + putExtra("sourceUrl", it) + } + } + + R.id.menu_clear -> { + viewModel.url?.let { + viewModel.clearArticles() + } + } + + R.id.menu_switch_layout -> { + viewModel.switchLayout() + upFragments() + } + + R.id.menu_read_record -> { + showDialogFragment() + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun upFragments() { + lifecycleScope.launch { + viewModel.rssSource?.sortUrls()?.let { + sortList.clear() + sortList.addAll(it) + } + if (sortList.size == 1) { + binding.tabLayout.gone() + } else { + binding.tabLayout.visible() + } + adapter.notifyDataSetChanged() + } + } + + private fun setSourceVariable() { + lifecycleScope.launch { + val source = viewModel.rssSource + if (source == null) { + toastOnUi("源不存在") + return@launch + } + val comment = + source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取") + val variable = withContext(Dispatchers.IO) { source.getVariable() } + showDialogFragment( + VariableDialog( + getString(R.string.set_source_variable), + source.getKey(), + variable, + comment + ) + ) + } + } + + override fun setVariable(key: String, variable: String?) { + viewModel.rssSource?.setVariable(variable) + } + + private inner class TabFragmentPageAdapter : + FragmentStatePagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getItemPosition(`object`: Any): Int { + return POSITION_NONE + } + + override fun getPageTitle(position: Int): CharSequence { + return sortList[position].first + } + + override fun getItem(position: Int): Fragment { + val sort = sortList[position] + return RssArticlesFragment(sort.first, sort.second) + } + + override fun getCount(): Int { + return sortList.size + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val fragment = super.instantiateItem(container, position) as Fragment + fragmentMap[sortList[position].first] = fragment + return fragment + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt new file mode 100755 index 000000000..e36933a84 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt @@ -0,0 +1,94 @@ +package io.legado.app.ui.rss.article + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssReadRecord +import io.legado.app.data.entities.RssSource +import io.legado.app.help.source.removeSortCache + + +class RssSortViewModel(application: Application) : BaseViewModel(application) { + var url: String? = null + var rssSource: RssSource? = null + val titleLiveData = MutableLiveData() + var order = System.currentTimeMillis() + val isGridLayout get() = rssSource?.articleStyle == 2 + + fun initData(intent: Intent, finally: () -> Unit) { + execute { + url = intent.getStringExtra("url") + url?.let { url -> + rssSource = appDb.rssSourceDao.getByKey(url) + rssSource?.let { + titleLiveData.postValue(it.sourceName) + } ?: let { + rssSource = RssSource(sourceUrl = url) + } + } + }.onFinally { + finally() + } + } + + fun switchLayout() { + rssSource?.let { + if (it.articleStyle < 2) { + it.articleStyle += 1 + } else { + it.articleStyle = 0 + } + execute { + appDb.rssSourceDao.update(it) + } + } + } + + fun read(rssArticle: RssArticle) { + execute { + val rssReadRecord = RssReadRecord( + record = rssArticle.link, + title = rssArticle.title, + readTime = System.currentTimeMillis() + ) + appDb.rssReadRecordDao.insertRecord(rssReadRecord) + } + } + + fun clearArticles() { + execute { + url?.let { + appDb.rssArticleDao.delete(it) + } + order = System.currentTimeMillis() + }.onSuccess { + + } + } + + fun clearSortCache(onFinally: () -> Unit) { + execute { + rssSource?.removeSortCache() + }.onFinally { + onFinally.invoke() + } + } + + fun getRecords(): List { + return appDb.rssReadRecordDao.getRecords() + } + + fun countRecords() : Int { + return appDb.rssReadRecordDao.countRecords + } + + fun deleteAllRecord() { + execute { + appDb.rssReadRecordDao.deleteAllRecord() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt new file mode 100755 index 000000000..714ff0cb2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt @@ -0,0 +1,184 @@ +@file:Suppress("DEPRECATION") + +package io.legado.app.ui.rss.favorites + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.lifecycleScope +import androidx.viewpager.widget.ViewPager +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.databinding.ActivityRssFavoritesBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.gone +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 收藏夹 + */ +class RssFavoritesActivity : BaseActivity() { + + override val binding by viewBinding(ActivityRssFavoritesBinding::inflate) + private val adapter by lazy { TabFragmentPageAdapter() } + private var groupList = mutableListOf() + private var groupsMenu: SubMenu? = null + private var currentGroup = "" + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + upFragments() + } + + override fun onResume() { + super.onResume() + //从ReadRssActivity退出时,判断是否需要重新定位tabLayout选中项 + if (currentGroup.isNotEmpty() && groupList.isNotEmpty()) { + var item = groupList.indexOf(currentGroup) + val currentItem = binding.viewPager.currentItem + //如果坐标没有变化,则结束 + if (item == currentItem) { + return + } + if (item == -1) { + item = currentItem + } + lifecycleScope.launch { + delay(100) + binding.tabLayout.getTabAt(item)?.select() + } + } + } + + private fun initView() { + binding.viewPager.adapter = adapter + binding.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int + ) { + } + + override fun onPageSelected(position: Int) { + currentGroup = groupList[position] + } + + override fun onPageScrollStateChanged(state: Int) {} + + }) + binding.tabLayout.setupWithViewPager(binding.viewPager) + binding.tabLayout.setSelectedTabIndicatorColor(accentColor) + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_favorites, menu) + groupsMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupsMenu() + return super.onCompatCreateOptionsMenu(menu) + } + + private fun upGroupsMenu() = groupsMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group) + groupList.forEachIndexed { index, it -> + subMenu.add(R.id.menu_group, Menu.NONE, index, it) + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + if (item.groupId == R.id.menu_group) { + binding.viewPager.setCurrentItem(item.order) + } else { + when (item.itemId) { + R.id.menu_del_group -> deleteGroup() + R.id.menu_del_all -> deleteAll() + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun upFragments() { + lifecycleScope.launch { + appDb.rssStarDao.flowGroups().catch { + AppLog.put("订阅分组数据获取失败\n${it.localizedMessage}", it) + }.distinctUntilChanged().flowOn(IO).collect { + groupList.clear() + groupList.addAll(it) + if (groupList.size == 1) { + binding.tabLayout.gone() + } else { + binding.tabLayout.visible() + } + if (groupsMenu != null) { + upGroupsMenu() + } + adapter.notifyDataSetChanged() + } + } + } + + private fun deleteGroup() { + if (groupList.isEmpty()) { + return + } + alert(R.string.draw) { + val item = binding.viewPager.currentItem + val group = groupList[item] + setMessage( + getString(R.string.sure_del) + "\n<" + group + ">" + getString(R.string.group) + ) + noButton() + yesButton { + appDb.rssStarDao.deleteByGroup(group) + } + } + } + + private fun deleteAll() { + alert(R.string.draw) { + setMessage( + getString(R.string.sure_del) + "\n<" + getString(R.string.all) + ">" + + getString(R.string.favorite) + ) + noButton() + yesButton { + appDb.rssStarDao.deleteAll() + } + } + } + + private inner class TabFragmentPageAdapter : + FragmentStatePagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getItemPosition(`object`: Any): Int { + return POSITION_NONE + } + + override fun getPageTitle(position: Int): CharSequence { + return groupList[position] + } + + override fun getItem(position: Int): Fragment { + val group = groupList[position] + return RssFavoritesFragment(group) + } + + override fun getCount(): Int { + return groupList.size + } + + } +} diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt new file mode 100755 index 000000000..f6b5e525b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt @@ -0,0 +1,91 @@ +package io.legado.app.ui.rss.favorites + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.ViewGroup +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RssStar +import io.legado.app.databinding.ItemRssArticleBinding +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +class RssFavoritesAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemRssArticleBinding { + return ItemRssArticleBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssArticleBinding, + item: RssStar, + payloads: MutableList + ) { + binding.run { + tvTitle.text = item.title + tvPubDate.text = item.pubDate + if (item.image.isNullOrBlank()) { + imageView.gone() + } else { + val options = + RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin) + ImageLoader.load(context, item.image) + .apply(options) + .addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + imageView.gone() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + imageView.visible() + return false + } + + }) + .into(imageView) + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticleBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + holder.itemView.setOnLongClickListener { + getItem(holder.layoutPosition)?.let { + callBack.delStar(it) + } + true + } + } + + interface CallBack { + fun readRss(rssStar: RssStar) + + fun delStar(rssStar: RssStar) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesDialog.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesDialog.kt new file mode 100755 index 000000000..b085ac6d8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesDialog.kt @@ -0,0 +1,74 @@ +package io.legado.app.ui.rss.favorites + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.data.entities.RssArticle +import io.legado.app.databinding.DialogRssFavoriteConfigBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class RssFavoritesDialog() : BaseDialogFragment(R.layout.dialog_rss_favorite_config, true) { + + constructor(rssArticle: RssArticle) : this() { + arguments = Bundle().apply { + putString("title", rssArticle.title) + putString("group", rssArticle.group) + } + } + + private val binding by viewBinding(DialogRssFavoriteConfigBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + val arguments = arguments ?: let { + dismiss() + return + } + + var title = arguments.getString("title") + var group = arguments.getString("group") + binding.run { + editTitle.setText(title) + editGroup.setText(group) + tvCancel.setOnClickListener { + dismiss() + } + tvOk.setOnClickListener { + val editTitle = editTitle.text.toString() + if (editTitle.isNotBlank()) { + title = editTitle + } + val editGroup = editGroup.text.toString() + if (editGroup.isNotBlank()) { + group = editGroup + } + callback?.updateFavorite(title, group) + dismiss() + } + tvFooterLeft.setOnClickListener { + callback?.deleteFavorite() + dismiss() + } + } + } + + val callback get() = (parentFragment as? Callback) ?: (activity as? Callback) + + interface Callback { + + fun updateFavorite(title: String?, group: String?) + + fun deleteFavorite() + + } + +} diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesFragment.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesFragment.kt new file mode 100755 index 000000000..f18b29847 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesFragment.kt @@ -0,0 +1,87 @@ +package io.legado.app.ui.rss.favorites + + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.VMBaseFragment +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssStar +import io.legado.app.databinding.FragmentRssArticlesBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.rss.read.ReadRssActivity +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +class RssFavoritesFragment() : VMBaseFragment(R.layout.fragment_rss_articles), + RssFavoritesAdapter.CallBack { + + constructor(group: String) : this() { + arguments = Bundle().apply { + putString("group", group) + } + } + + private val binding by viewBinding(FragmentRssArticlesBinding::bind) + override val viewModel by viewModels() + private val adapter: RssFavoritesAdapter by lazy { + RssFavoritesAdapter(requireContext(), this@RssFavoritesFragment) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + loadArticles() + } + + private fun initView() = binding.run { + refreshLayout.isEnabled = false + recyclerView.setEdgeEffectColor(primaryColor) + recyclerView.layoutManager = run { + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + LinearLayoutManager(requireContext()) + } + recyclerView.adapter = adapter + recyclerView.applyNavigationBarPadding() + } + + private fun loadArticles() { + lifecycleScope.launch { + val group = arguments?.getString("group") ?: "默认分组" + appDb.rssStarDao.flowByGroup(group).catch { + AppLog.put("订阅文章界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + override fun readRss(rssStar: RssStar) { + startActivity { + putExtra("title", rssStar.title) + putExtra("origin", rssStar.origin) + putExtra("link", rssStar.link) + } + } + + override fun delStar(rssStar: RssStar) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n<" + rssStar.title + ">") + noButton() + yesButton { + appDb.rssStarDao.delete(rssStar.origin, rssStar.link) + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesViewModel.kt new file mode 100755 index 000000000..bc028698a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesViewModel.kt @@ -0,0 +1,9 @@ +package io.legado.app.ui.rss.favorites + +import android.app.Application +import io.legado.app.base.BaseViewModel + + +class RssFavoritesViewModel(application: Application) : BaseViewModel(application) { + +} diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt new file mode 100755 index 000000000..fd6fe798c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -0,0 +1,532 @@ +package io.legado.app.ui.rss.read + +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.net.Uri +import android.net.http.SslError +import android.os.Bundle +import android.os.SystemClock +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.WindowManager +import android.webkit.JavascriptInterface +import android.webkit.SslErrorHandler +import android.webkit.URLUtil +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.size +import androidx.lifecycle.lifecycleScope +import com.script.rhino.runScriptWithContext +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.imagePathKey +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ActivityRssReadBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.http.CookieManager +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.model.Download +import io.legado.app.ui.association.OnLineImportActivity +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.rss.favorites.RssFavoritesDialog +import io.legado.app.utils.ACache +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.gone +import io.legado.app.utils.invisible +import io.legado.app.utils.isTrue +import io.legado.app.utils.keepScreenOn +import io.legado.app.utils.longSnackbar +import io.legado.app.utils.openUrl +import io.legado.app.utils.setDarkeningAllowed +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.setTintMutate +import io.legado.app.utils.share +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.startActivity +import io.legado.app.utils.textArray +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.toggleSystemBar +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.launch +import org.apache.commons.text.StringEscapeUtils +import org.jsoup.Jsoup +import splitties.views.bottomPadding +import java.io.ByteArrayInputStream +import java.net.URLDecoder +import java.util.regex.PatternSyntaxException + +/** + * rss阅读界面 + */ +class ReadRssActivity : VMBaseActivity(), + RssFavoritesDialog.Callback { + + override val binding by viewBinding(ActivityRssReadBinding::inflate) + override val viewModel by viewModels() + + private var starMenuItem: MenuItem? = null + private var ttsMenuItem: MenuItem? = null + private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null + private val selectImageDir = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + ACache.get().put(imagePathKey, uri.toString()) + viewModel.saveImage(it.value, uri) + } + } + private val rssJsExtensions by lazy { RssJsExtensions(this) } + + fun getSource(): RssSource? { + return viewModel.rssSource + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + viewModel.upStarMenuData.observe(this) { upStarMenu() } + viewModel.upTtsMenuData.observe(this) { upTtsMenu(it) } + binding.titleBar.title = intent.getStringExtra("title") + initView() + initWebView() + initLiveData() + viewModel.initData(intent) + onBackPressedDispatcher.addCallback(this) { + if (binding.customWebView.size > 0) { + customWebViewCallback?.onCustomViewHidden() + return@addCallback + } else if (binding.webView.canGoBack() + && binding.webView.copyBackForwardList().size > 1 + ) { + binding.webView.goBack() + return@addCallback + } + finish() + } + } + + @Suppress("DEPRECATION") + @SuppressLint("SwitchIntDef") + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + when (newConfig.orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + } + + Configuration.ORIENTATION_PORTRAIT -> { + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) + } + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_read, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + starMenuItem = menu.findItem(R.id.menu_rss_star) + ttsMenuItem = menu.findItem(R.id.menu_aloud) + upStarMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_login)?.isVisible = !viewModel.rssSource?.loginUrl.isNullOrBlank() + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_rss_refresh -> viewModel.refresh { + binding.webView.reload() + } + + R.id.menu_rss_star -> { + viewModel.addFavorite() + viewModel.rssArticle?.let { + showDialogFragment(RssFavoritesDialog(it)) + } + } + + R.id.menu_share_it -> { + binding.webView.url?.let { + share(it) + } ?: viewModel.rssArticle?.let { + share(it.link) + } ?: toastOnUi(R.string.null_url) + } + + R.id.menu_aloud -> readAloud() + R.id.menu_login -> startActivity { + putExtra("type", "rssSource") + putExtra("key", viewModel.rssSource?.sourceUrl) + } + + R.id.menu_browser_open -> binding.webView.url?.let { + openUrl(it) + } ?: toastOnUi("url null") + } + return super.onCompatOptionsItemSelected(item) + } + + override fun updateFavorite(title: String?, group: String?) { + viewModel.rssArticle?.let { + if (title != null) { + it.title = title + } + if (group != null) { + it.group = group + } + } + viewModel.updateFavorite() + } + + override fun deleteFavorite() { + viewModel.delFavorite() + } + + @JavascriptInterface + fun isNightTheme(): Boolean { + return AppConfig.isNightTheme + } + + private fun initView() { + binding.root.setOnApplyWindowInsetsListenerCompat { view, windowInsets -> + val typeMask = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() + val insets = windowInsets.getInsets(typeMask) + view.bottomPadding = insets.bottom + windowInsets + } + } + + @SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") + private fun initWebView() { + binding.progressBar.fontColor = accentColor + binding.webView.webChromeClient = CustomWebChromeClient() + binding.webView.webViewClient = CustomWebViewClient() + binding.webView.settings.apply { + mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + domStorageEnabled = true + allowContentAccess = true + builtInZoomControls = true + displayZoomControls = false + setDarkeningAllowed(AppConfig.isNightTheme) + } + binding.webView.addJavascriptInterface(this, "thisActivity") + binding.webView.setOnLongClickListener { + val hitTestResult = binding.webView.hitTestResult + if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE || + hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE + ) { + hitTestResult.extra?.let { webPic -> + selector( + arrayListOf( + SelectItem(getString(R.string.action_save), "save"), + SelectItem(getString(R.string.select_folder), "selectFolder") + ) + ) { _, charSequence, _ -> + when (charSequence.value) { + "save" -> saveImage(webPic) + "selectFolder" -> selectSaveFolder(null) + } + } + return@setOnLongClickListener true + } + } + return@setOnLongClickListener false + } + binding.webView.setDownloadListener { url, _, contentDisposition, _, _ -> + var fileName = URLUtil.guessFileName(url, contentDisposition, null) + fileName = URLDecoder.decode(fileName, "UTF-8") + binding.llView.longSnackbar(fileName, getString(R.string.action_download)) { + Download.start(this, url, fileName) + } + } + + } + + private fun saveImage(webPic: String) { + val path = ACache.get().getAsString(imagePathKey) + if (path.isNullOrEmpty()) { + selectSaveFolder(webPic) + } else { + viewModel.saveImage(webPic, Uri.parse(path)) + } + } + + private fun selectSaveFolder(webPic: String?) { + val default = arrayListOf>() + val path = ACache.get().getAsString(imagePathKey) + if (!path.isNullOrEmpty()) { + default.add(SelectItem(path, -1)) + } + selectImageDir.launch { + otherActions = default + value = webPic + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun initLiveData() { + viewModel.contentLiveData.observe(this) { content -> + viewModel.rssArticle?.let { + upJavaScriptEnable() + val url = NetworkUtils.getAbsoluteURL(it.origin, it.link) + val html = viewModel.clHtml(content) + binding.webView.settings.userAgentString = + viewModel.headerMap[AppConst.UA_NAME] ?: AppConfig.userAgent + if (viewModel.rssSource?.loadWithBaseUrl == true) { + binding.webView + .loadDataWithBaseURL(url, html, "text/html", "utf-8", url)//不想用baseUrl进else + } else { + binding.webView + .loadDataWithBaseURL(null, html, "text/html;charset=utf-8", "utf-8", url) + } + } + } + viewModel.urlLiveData.observe(this) { + upJavaScriptEnable() + CookieManager.applyToWebView(it.url) + binding.webView.settings.userAgentString = it.getUserAgent() + binding.webView.loadUrl(it.url, it.headerMap) + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun upJavaScriptEnable() { + if (viewModel.rssSource?.enableJs == true) { + binding.webView.settings.javaScriptEnabled = true + } + } + + private fun upStarMenu() { + starMenuItem?.isVisible = viewModel.rssArticle != null + if (viewModel.rssStar != null) { + starMenuItem?.setIcon(R.drawable.ic_star) + starMenuItem?.setTitle(R.string.in_favorites) + } else { + starMenuItem?.setIcon(R.drawable.ic_star_border) + starMenuItem?.setTitle(R.string.out_favorites) + } + starMenuItem?.icon?.setTintMutate(primaryTextColor) + } + + private fun upTtsMenu(isPlaying: Boolean) { + lifecycleScope.launch { + if (isPlaying) { + ttsMenuItem?.setIcon(R.drawable.ic_stop_black_24dp) + ttsMenuItem?.setTitle(R.string.aloud_stop) + } else { + ttsMenuItem?.setIcon(R.drawable.ic_volume_up) + ttsMenuItem?.setTitle(R.string.read_aloud) + } + ttsMenuItem?.icon?.setTintMutate(primaryTextColor) + } + } + + @SuppressLint("SetJavaScriptEnabled") + private fun readAloud() { + if (viewModel.tts?.isSpeaking == true) { + viewModel.tts?.stop() + upTtsMenu(false) + } else { + binding.webView.settings.javaScriptEnabled = true + binding.webView.evaluateJavascript("document.documentElement.outerHTML") { + val html = StringEscapeUtils.unescapeJson(it) + .replace("^\"|\"$".toRegex(), "") + viewModel.readAloud( + Jsoup.parse(html) + .textArray() + .joinToString("\n") + ) + } + } + } + + override fun onDestroy() { + super.onDestroy() + binding.webView.destroy() + } + + inner class CustomWebChromeClient : WebChromeClient() { + + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + binding.progressBar.setDurProgress(newProgress) + binding.progressBar.gone(newProgress == 100) + } + + override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR + binding.llView.invisible() + binding.customWebView.addView(view) + customWebViewCallback = callback + keepScreenOn(true) + toggleSystemBar(false) + } + + override fun onHideCustomView() { + binding.customWebView.removeAllViews() + binding.llView.visible() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + keepScreenOn(false) + toggleSystemBar(true) + } + } + + inner class CustomWebViewClient : WebViewClient() { + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + return shouldOverrideUrlLoading(request.url) + } + + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "KotlinRedundantDiagnosticSuppress") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return shouldOverrideUrlLoading(Uri.parse(url)) + } + + /** + * 如果有黑名单,黑名单匹配返回空白, + * 没有黑名单再判断白名单,在白名单中的才通过, + * 都没有不做处理 + */ + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + val url = request.url.toString() + val source = viewModel.rssSource ?: return super.shouldInterceptRequest(view, request) + val blacklist = source.contentBlacklist?.splitNotBlank(",") + if (!blacklist.isNullOrEmpty()) { + blacklist.forEach { + try { + if (url.startsWith(it) || url.matches(it.toRegex())) { + return createEmptyResource() + } + } catch (e: PatternSyntaxException) { + AppLog.put("黑名单规则正则语法错误 源名称:${source.sourceName} 正则:$it", e) + } + } + } else { + val whitelist = source.contentWhitelist?.splitNotBlank(",") + if (!whitelist.isNullOrEmpty()) { + whitelist.forEach { + try { + if (url.startsWith(it) || url.matches(it.toRegex())) { + return super.shouldInterceptRequest(view, request) + } + } catch (e: PatternSyntaxException) { + val msg = "白名单规则正则语法错误 源名称:${source.sourceName} 正则:$it" + AppLog.put(msg, e) + } + } + return createEmptyResource() + } + } + return super.shouldInterceptRequest(view, request) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + view.title?.let { title -> + if (title != url + && title != view.url + && title.isNotBlank() + && url != "about:blank" + && !url.contains(title) + ) { + binding.titleBar.title = title + } else { + binding.titleBar.title = intent.getStringExtra("title") + } + } + viewModel.rssSource?.injectJs?.let { + if (it.isNotBlank()) { + view.evaluateJavascript(it, null) + } + } + } + + private fun createEmptyResource(): WebResourceResponse { + return WebResourceResponse( + "text/plain", + "utf-8", + ByteArrayInputStream("".toByteArray()) + ) + } + + private fun shouldOverrideUrlLoading(url: Uri): Boolean { + val source = viewModel.rssSource + val js = source?.shouldOverrideUrlLoading + if (!js.isNullOrBlank()) { + val t = SystemClock.uptimeMillis() + val result = kotlin.runCatching { + runScriptWithContext(lifecycleScope.coroutineContext) { + source.evalJS(js) { + put("java", rssJsExtensions) + put("url", url.toString()) + }.toString() + } + }.onFailure { + AppLog.put("${source.getTag()}: url跳转拦截js出错", it) + }.getOrNull() + if (SystemClock.uptimeMillis() - t > 30) { + AppLog.put("${source.getTag()}: url跳转拦截js执行耗时过长") + } + if (result.isTrue()) { + return true + } + } + when (url.scheme) { + "http", "https", "jsbridge" -> { + return false + } + + "legado", "yuedu" -> { + startActivity { + data = url + } + return true + } + + else -> { + binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) { + openUrl(url) + } + return true + } + } + } + + @SuppressLint("WebViewClientOnReceivedSslError") + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + handler?.proceed() + } + + } + +} diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt new file mode 100755 index 000000000..1d5d7d0ba --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -0,0 +1,251 @@ +package io.legado.app.ui.rss.read + +import android.app.Application +import android.content.Intent +import android.net.Uri +import android.util.Base64 +import android.webkit.URLUtil +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.script.rhino.runScriptWithContext +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppConst.imagePathKey +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssSource +import io.legado.app.data.entities.RssStar +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.TTS +import io.legado.app.help.http.newCallResponseBody +import io.legado.app.help.http.okHttpClient +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.rss.Rss +import io.legado.app.utils.ACache +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.writeBytes +import kotlinx.coroutines.Dispatchers.IO +import splitties.init.appCtx +import java.util.Date +import kotlin.coroutines.coroutineContext + + +class ReadRssViewModel(application: Application) : BaseViewModel(application) { + var rssSource: RssSource? = null + var rssArticle: RssArticle? = null + var tts: TTS? = null + val contentLiveData = MutableLiveData() + val urlLiveData = MutableLiveData() + var rssStar: RssStar? = null + val upTtsMenuData = MutableLiveData() + val upStarMenuData = MutableLiveData() + var headerMap: Map = emptyMap() + + fun initData(intent: Intent) { + execute { + val origin = intent.getStringExtra("origin") ?: return@execute + val link = intent.getStringExtra("link") + rssSource = appDb.rssSourceDao.getByKey(origin) + headerMap = runScriptWithContext { + rssSource?.getHeaderMap() ?: emptyMap() + } + if (link != null) { + rssStar = appDb.rssStarDao.get(origin, link) + rssArticle = rssStar?.toRssArticle() ?: appDb.rssArticleDao.get(origin, link) + val rssArticle = rssArticle ?: return@execute + if (!rssArticle.description.isNullOrBlank()) { + contentLiveData.postValue(rssArticle.description!!) + } else { + rssSource?.let { + val ruleContent = it.ruleContent + if (!ruleContent.isNullOrBlank()) { + loadContent(rssArticle, ruleContent) + } else { + loadUrl(rssArticle.link, rssArticle.origin) + } + } ?: loadUrl(rssArticle.link, rssArticle.origin) + } + } else { + val ruleContent = rssSource?.ruleContent + if (ruleContent.isNullOrBlank()) { + loadUrl(origin, origin) + } else { + val rssArticle = RssArticle() + rssArticle.origin = origin + rssArticle.link = origin + rssArticle.title = rssSource!!.sourceName + loadContent(rssArticle, ruleContent) + } + } + }.onFinally { + upStarMenuData.postValue(true) + } + } + + private suspend fun loadUrl(url: String, baseUrl: String) { + val analyzeUrl = AnalyzeUrl( + mUrl = url, + baseUrl = baseUrl, + source = rssSource, + coroutineContext = coroutineContext, + hasLoginHeader = false + ) + urlLiveData.postValue(analyzeUrl) + } + + private fun loadContent(rssArticle: RssArticle, ruleContent: String) { + val source = rssSource ?: return + Rss.getContent(viewModelScope, rssArticle, ruleContent, source) + .onSuccess(IO) { body -> + rssArticle.description = body + appDb.rssArticleDao.insert(rssArticle) + rssStar?.let { + it.description = body + appDb.rssStarDao.insert(it) + } + contentLiveData.postValue(body) + }.onError { + contentLiveData.postValue("加载正文失败\n${it.stackTraceToString()}") + } + } + + fun refresh(finish: () -> Unit) { + val rssArticle = rssArticle ?: return finish.invoke() + if (!rssArticle.description.isNullOrBlank()) { + return finish.invoke() + } + val rssSource = rssSource ?: let { + appCtx.toastOnUi("订阅源不存在") + return finish.invoke() + } + val ruleContent = rssSource.ruleContent + if (!ruleContent.isNullOrBlank()) { + loadContent(rssArticle, ruleContent) + } else { + finish.invoke() + } + } + + fun favorite() { + execute { + rssStar?.let { + appDb.rssStarDao.delete(it.origin, it.link) + rssStar = null + } ?: rssArticle?.toStar()?.let { + appDb.rssStarDao.insert(it) + rssStar = it + } + }.onSuccess { + upStarMenuData.postValue(true) + } + } + + fun addFavorite() { + execute { + rssStar ?: rssArticle?.toStar()?.let { + appDb.rssStarDao.insert(it) + rssStar = it + } + }.onSuccess { + upStarMenuData.postValue(true) + } + } + + fun updateFavorite() { + execute { + rssArticle?.toStar()?.let { + appDb.rssStarDao.update(it) + rssStar = it + } + }.onSuccess { + upStarMenuData.postValue(true) + } + } + + fun delFavorite() { + execute { + rssStar?.let { + appDb.rssStarDao.delete(it.origin, it.link) + rssStar = null + } + }.onSuccess { + upStarMenuData.postValue(true) + } + } + + fun saveImage(webPic: String?, uri: Uri) { + webPic ?: return + execute { + val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg" + val byteArray = webData2bitmap(webPic) ?: throw NoStackTraceException("NULL") + uri.writeBytes(context, fileName, byteArray) + }.onError { + ACache.get().remove(imagePathKey) + context.toastOnUi("保存图片失败:${it.localizedMessage}") + }.onSuccess { + context.toastOnUi("保存成功") + } + } + + private suspend fun webData2bitmap(data: String): ByteArray? { + return if (URLUtil.isValidUrl(data)) { + okHttpClient.newCallResponseBody { + url(data) + }.bytes() + } else { + Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT) + } + } + + fun clHtml(content: String): String { + return when { + !rssSource?.style.isNullOrEmpty() -> { + """ + + $content + """.trimIndent() + } + + content.contains(" + $content + """.trimIndent() + } + } + } + + @Synchronized + fun readAloud(text: String) { + if (tts == null) { + tts = TTS().apply { + setSpeakStateListener(object : TTS.SpeakStateListener { + override fun onStart() { + upTtsMenuData.postValue(true) + } + + override fun onDone() { + upTtsMenuData.postValue(false) + } + }) + } + } + tts?.speak(text) + } + + override fun onCleared() { + super.onCleared() + tts?.clearTts() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt b/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt new file mode 100755 index 000000000..5df98b9b7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/read/RssJsExtensions.kt @@ -0,0 +1,24 @@ +package io.legado.app.ui.rss.read + +import io.legado.app.data.entities.BaseSource +import io.legado.app.help.JsExtensions +import io.legado.app.ui.association.AddToBookshelfDialog +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.utils.showDialogFragment + +@Suppress("unused") +class RssJsExtensions(private val activity: ReadRssActivity) : JsExtensions { + + override fun getSource(): BaseSource? { + return activity.getSource() + } + + fun searchBook(key: String) { + SearchActivity.start(activity, key) + } + + fun addBook(bookUrl: String) { + activity.showDialogFragment(AddToBookshelfDialog(bookUrl)) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt b/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt new file mode 100755 index 000000000..0e6e1b40b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt @@ -0,0 +1,17 @@ +package io.legado.app.ui.rss.read + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.webkit.WebView + +class VisibleWebView( + context: Context, + attrs: AttributeSet? = null +) : WebView(context, attrs) { + + override fun onWindowVisibilityChanged(visibility: Int) { + super.onWindowVisibilityChanged(View.VISIBLE) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt new file mode 100755 index 000000000..b73219fc9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugActivity.kt @@ -0,0 +1,78 @@ +package io.legado.app.ui.rss.source.debug + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.SearchView +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.databinding.ActivitySourceDebugBinding +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.gone +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.launch + + +class RssSourceDebugActivity : VMBaseActivity() { + + override val binding by viewBinding(ActivitySourceDebugBinding::inflate) + override val viewModel by viewModels() + + private val adapter by lazy { RssSourceDebugAdapter(this) } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initRecyclerView() + initSearchView() + viewModel.observe { state, msg -> + lifecycleScope.launch { + adapter.addItem(msg) + if (state == -1 || state == 1000) { + binding.rotateLoading.gone() + } + } + } + viewModel.initData(intent.getStringExtra("key")) { + startDebug() + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_source_debug, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_list_src -> showDialogFragment(TextDialog("Html", viewModel.listSrc)) + R.id.menu_content_src -> showDialogFragment(TextDialog("Html", viewModel.contentSrc)) + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + binding.rotateLoading.loadingColor = accentColor + } + + private fun initSearchView() { + binding.titleBar.findViewById(R.id.search_view).gone() + } + + private fun startDebug() { + adapter.clearItems() + viewModel.rssSource?.let { + binding.rotateLoading.visible() + viewModel.startDebug(it) + } ?: toastOnUi(R.string.error_no_source) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt new file mode 100755 index 000000000..dc92e12d6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt @@ -0,0 +1,44 @@ +package io.legado.app.ui.rss.source.debug + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemLogBinding + +class RssSourceDebugAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemLogBinding { + return ItemLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemLogBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + if (textView.getTag(R.id.tag1) == null) { + val listener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + textView.isCursorVisible = false + textView.isCursorVisible = true + } + + override fun onViewDetachedFromWindow(v: View) {} + } + textView.addOnAttachStateChangeListener(listener) + textView.setTag(R.id.tag1, listener) + } + textView.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) { + //nothing + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt new file mode 100755 index 000000000..b992ab8ca --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt @@ -0,0 +1,50 @@ +package io.legado.app.ui.rss.source.debug + +import android.app.Application +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.model.Debug + +class RssSourceDebugModel(application: Application) : BaseViewModel(application), + Debug.Callback { + var rssSource: RssSource? = null + private var callback: ((Int, String) -> Unit)? = null + var listSrc: String? = null + var contentSrc: String? = null + + fun initData(sourceUrl: String?, finally: () -> Unit) { + sourceUrl?.let { + execute { + rssSource = appDb.rssSourceDao.getByKey(sourceUrl) + }.onFinally { + finally() + } + } + } + + fun observe(callback: (Int, String) -> Unit) { + this.callback = callback + } + + fun startDebug(source: RssSource) { + execute { + Debug.callback = this@RssSourceDebugModel + Debug.startDebug(this, source) + } + } + + override fun printLog(state: Int, msg: String) { + when (state) { + 10 -> listSrc = msg + 20 -> contentSrc = msg + else -> callback?.invoke(state, msg) + } + } + + override fun onCleared() { + super.onCleared() + Debug.cancelDebug(true) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt new file mode 100755 index 000000000..b576f4acc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt @@ -0,0 +1,407 @@ +package io.legado.app.ui.rss.source.edit + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.widget.EditText +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.tabs.TabLayout +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ActivityRssSourceEditBinding +import io.legado.app.help.config.LocalConfig +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.login.SourceLoginActivity +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity +import io.legado.app.ui.widget.dialog.UrlOptionDialog +import io.legado.app.ui.widget.dialog.VariableDialog +import io.legado.app.ui.widget.keyboard.KeyboardToolPop +import io.legado.app.ui.widget.text.EditEntity +import io.legado.app.utils.GSON +import io.legado.app.utils.imeHeight +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.isTrue +import io.legado.app.utils.launch +import io.legado.app.utils.navigationBarHeight +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import io.legado.app.utils.share +import io.legado.app.utils.shareWithQr +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import splitties.views.bottomPadding + +class RssSourceEditActivity : + VMBaseActivity(), + KeyboardToolPop.CallBack, + VariableDialog.Callback { + + override val binding by viewBinding(ActivityRssSourceEditBinding::inflate) + override val viewModel by viewModels() + private val softKeyboardTool by lazy { + KeyboardToolPop(this, lifecycleScope, binding.root, this) + } + private val adapter by lazy { RssSourceEditAdapter() } + private val sourceEntities: ArrayList = ArrayList() + private val listEntities: ArrayList = ArrayList() + private val webViewEntities: ArrayList = ArrayList() + private val selectDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + if (uri.isContentScheme()) { + sendText(uri.toString()) + } else { + sendText(uri.path.toString()) + } + } + } + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it?.let { + viewModel.importSource(it) { source: RssSource -> + upSourceView(source) + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + softKeyboardTool.attachToWindow(window) + initView() + viewModel.initData(intent) { + upSourceView(viewModel.rssSource) + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + if (!LocalConfig.ruleHelpVersionIsLast) { + showHelp("ruleHelp") + } + } + + override fun finish() { + val source = getRssSource() + if (!source.equal(viewModel.rssSource ?: RssSource())) { + alert(R.string.exit) { + setMessage(R.string.exit_no_save) + positiveButton(R.string.yes) + negativeButton(R.string.no) { + super.finish() + } + } + } else { + super.finish() + } + } + + override fun onDestroy() { + super.onDestroy() + softKeyboardTool.dismiss() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.source_edit, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { + menu.findItem(R.id.menu_login)?.isVisible = !viewModel.rssSource?.loginUrl.isNullOrBlank() + menu.findItem(R.id.menu_auto_complete)?.isChecked = viewModel.autoComplete + return super.onMenuOpened(featureId, menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> viewModel.save(getRssSource()) { + setResult(RESULT_OK) + finish() + } + + R.id.menu_debug_source -> viewModel.save(getRssSource()) { source -> + startActivity { + putExtra("key", source.sourceUrl) + } + } + + R.id.menu_login -> viewModel.save(getRssSource()) { + startActivity { + putExtra("type", "rssSource") + putExtra("key", it.sourceUrl) + } + } + + R.id.menu_set_source_variable -> setSourceVariable() + R.id.menu_clear_cookie -> viewModel.clearCookie(getRssSource().sourceUrl) + R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete + R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource())) + R.id.menu_qr_code_camera -> qrCodeResult.launch() + R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) } + R.id.menu_share_str -> share(GSON.toJson(getRssSource())) + R.id.menu_share_qr -> shareWithQr( + GSON.toJson(getRssSource()), + getString(R.string.share_rss_source), + ErrorCorrectionLevel.L + ) + + R.id.menu_help -> showHelp("ruleHelp") + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_base) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + setText(R.string.source_tab_list) + }) + binding.tabLayout.addTab(binding.tabLayout.newTab().apply { + text = "WEB_VIEW" + }) + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.adapter = adapter + binding.tabLayout.setBackgroundColor(backgroundColor) + binding.tabLayout.setSelectedTabIndicatorColor(accentColor) + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab?) { + + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + + } + + override fun onTabSelected(tab: TabLayout.Tab?) { + setEditEntities(tab?.position) + } + }) + binding.recyclerView.setOnApplyWindowInsetsListenerCompat { view, windowInsets -> + val navigationBarHeight = windowInsets.navigationBarHeight + val imeHeight = windowInsets.imeHeight + view.bottomPadding = if (imeHeight == 0) navigationBarHeight else 0 + softKeyboardTool.initialPadding = imeHeight + windowInsets + } + } + + private fun setEditEntities(tabPosition: Int?) { + when (tabPosition) { + 1 -> adapter.editEntities = listEntities + 2 -> adapter.editEntities = webViewEntities + else -> adapter.editEntities = sourceEntities + } + binding.recyclerView.scrollToPosition(0) + } + + private fun upSourceView(rssSource: RssSource?) { + val rs = rssSource ?: RssSource() + rs.let { + binding.cbIsEnable.isChecked = rs.enabled + binding.cbSingleUrl.isChecked = rs.singleUrl + binding.cbIsEnableCookie.isChecked = rs.enabledCookieJar == true + } + sourceEntities.clear() + sourceEntities.apply { + add(EditEntity("sourceName", rs.sourceName, R.string.source_name)) + add(EditEntity("sourceUrl", rs.sourceUrl, R.string.source_url)) + add(EditEntity("sourceIcon", rs.sourceIcon, R.string.source_icon)) + add(EditEntity("sourceGroup", rs.sourceGroup, R.string.source_group)) + add(EditEntity("sourceComment", rs.sourceComment, R.string.comment)) + add(EditEntity("sortUrl", rs.sortUrl, R.string.sort_url)) + add(EditEntity("loginUrl", rs.loginUrl, R.string.login_url)) + add(EditEntity("loginUi", rs.loginUi, R.string.login_ui)) + add(EditEntity("loginCheckJs", rs.loginCheckJs, R.string.login_check_js)) + add(EditEntity("coverDecodeJs", rs.coverDecodeJs, R.string.cover_decode_js)) + add(EditEntity("header", rs.header, R.string.source_http_header)) + add(EditEntity("variableComment", rs.variableComment, R.string.variable_comment)) + add(EditEntity("concurrentRate", rs.concurrentRate, R.string.concurrent_rate)) + add(EditEntity("jsLib", rs.jsLib, "jsLib")) + } + listEntities.clear() + listEntities.apply { + add(EditEntity("ruleArticles", rs.ruleArticles, R.string.r_articles)) + add(EditEntity("ruleNextPage", rs.ruleNextPage, R.string.r_next)) + add(EditEntity("ruleTitle", rs.ruleTitle, R.string.r_title)) + add(EditEntity("rulePubDate", rs.rulePubDate, R.string.r_date)) + add(EditEntity("ruleDescription", rs.ruleDescription, R.string.r_description)) + add(EditEntity("ruleImage", rs.ruleImage, R.string.r_image)) + add(EditEntity("ruleLink", rs.ruleLink, R.string.r_link)) + } + webViewEntities.clear() + webViewEntities.apply { + add( + EditEntity( + "enableJs", + rs.enableJs.toString(), + R.string.enable_js, + EditEntity.ViewType.checkBox + ) + ) + add( + EditEntity( + "loadWithBaseUrl", + rs.loadWithBaseUrl.toString(), + R.string.load_with_base_url, + EditEntity.ViewType.checkBox + ) + ) + add(EditEntity("ruleContent", rs.ruleContent, R.string.r_content)) + add(EditEntity("style", rs.style, R.string.r_style)) + add(EditEntity("injectJs", rs.injectJs, R.string.r_inject_js)) + add(EditEntity("contentWhitelist", rs.contentWhitelist, R.string.c_whitelist)) + add(EditEntity("contentBlacklist", rs.contentBlacklist, R.string.c_blacklist)) + add( + EditEntity( + "shouldOverrideUrlLoading", + rs.shouldOverrideUrlLoading, + "url跳转拦截(js, 返回true拦截,js变量url,可以通过js打开url,比如调用阅读搜索,添加书架等,简化规则写法,不用webView js注入)" + ) + ) + } + binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0)) + setEditEntities(0) + } + + private fun getRssSource(): RssSource { + val source = viewModel.rssSource?.copy() ?: RssSource() + source.enabled = binding.cbIsEnable.isChecked + source.singleUrl = binding.cbSingleUrl.isChecked + source.enabledCookieJar = binding.cbIsEnableCookie.isChecked + sourceEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "sourceName" -> source.sourceName = it.value ?: "" + "sourceUrl" -> source.sourceUrl = it.value ?: "" + "sourceIcon" -> source.sourceIcon = it.value ?: "" + "sourceGroup" -> source.sourceGroup = it.value + "sourceComment" -> source.sourceComment = it.value + "loginUrl" -> source.loginUrl = it.value + "loginUi" -> source.loginUi = it.value + "loginCheckJs" -> source.loginCheckJs = it.value + "coverDecodeJs" -> source.coverDecodeJs = it.value + "header" -> source.header = it.value + "variableComment" -> source.variableComment = it.value + "concurrentRate" -> source.concurrentRate = it.value + "sortUrl" -> source.sortUrl = it.value + "jsLib" -> source.jsLib = it.value + } + } + listEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "ruleArticles" -> source.ruleArticles = it.value + "ruleNextPage" -> source.ruleNextPage = + viewModel.ruleComplete(it.value, source.ruleArticles, 2) + + "ruleTitle" -> source.ruleTitle = + viewModel.ruleComplete(it.value, source.ruleArticles) + + "rulePubDate" -> source.rulePubDate = + viewModel.ruleComplete(it.value, source.ruleArticles) + + "ruleDescription" -> source.ruleDescription = + viewModel.ruleComplete(it.value, source.ruleArticles) + + "ruleImage" -> source.ruleImage = + viewModel.ruleComplete(it.value, source.ruleArticles, 3) + + "ruleLink" -> source.ruleLink = + viewModel.ruleComplete(it.value, source.ruleArticles) + } + } + webViewEntities.forEach { + it.value = it.value?.takeIf { s -> s.isNotBlank() } + when (it.key) { + "enableJs" -> source.enableJs = it.value.isTrue() + "loadWithBaseUrl" -> source.loadWithBaseUrl = it.value.isTrue() + "ruleContent" -> source.ruleContent = + viewModel.ruleComplete(it.value, source.ruleArticles) + + "style" -> source.style = it.value + "injectJs" -> source.injectJs = it.value + "contentWhitelist" -> source.contentWhitelist = it.value + "contentBlacklist" -> source.contentBlacklist = it.value + "shouldOverrideUrlLoading" -> source.shouldOverrideUrlLoading = it.value + } + } + return source + } + + private fun setSourceVariable() { + viewModel.save(getRssSource()) { source -> + lifecycleScope.launch { + val comment = + source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取") + val variable = withContext(Dispatchers.IO) { source.getVariable() } + showDialogFragment( + VariableDialog( + getString(R.string.set_source_variable), + source.getKey(), + variable, + comment + ) + ) + } + } + } + + override fun setVariable(key: String, variable: String?) { + viewModel.rssSource?.setVariable(variable) + } + + override fun helpActions(): List> { + return arrayListOf( + SelectItem("插入URL参数", "urlOption"), + SelectItem("订阅源教程", "ruleHelp"), + SelectItem("js教程", "jsHelp"), + SelectItem("正则教程", "regexHelp"), + SelectItem("选择文件", "selectFile"), + ) + } + + override fun onHelpActionSelect(action: String) { + when (action) { + "urlOption" -> UrlOptionDialog(this) { + sendText(it) + }.show() + + "ruleHelp" -> showHelp("ruleHelp") + "jsHelp" -> showHelp("jsHelp") + "regexHelp" -> showHelp("regexHelp") + "selectFile" -> selectDoc.launch { + mode = HandleFileContract.FILE + } + } + } + + override fun sendText(text: String) { + if (text.isBlank()) return + val view = window.decorView.findFocus() + if (view is EditText) { + val start = view.selectionStart + val end = view.selectionEnd + val edit = view.editableText//获取EditText的文字 + if (start < 0 || start >= edit.length) { + edit.append(text) + } else if (start > end) { + edit.replace(end, start, text) + } else { + edit.replace(start, end, text)//光标所在位置插入文字 + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt new file mode 100755 index 000000000..95c15f786 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt @@ -0,0 +1,131 @@ +package io.legado.app.ui.rss.source.edit + +import android.annotation.SuppressLint +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.databinding.ItemSourceEditBinding +import io.legado.app.databinding.ItemSourceEditCheckBoxBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.ui.widget.code.addJsPattern +import io.legado.app.ui.widget.code.addJsonPattern +import io.legado.app.ui.widget.code.addLegadoPattern +import io.legado.app.ui.widget.text.EditEntity +import io.legado.app.utils.isTrue + +class RssSourceEditAdapter : RecyclerView.Adapter() { + + val editEntityMaxLine = AppConfig.sourceEditMaxLine + + var editEntities: ArrayList = ArrayList() + @SuppressLint("NotifyDataSetChanged") + set(value) { + field = value + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int { + val item = editEntities[position] + return item.viewType + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + EditEntity.ViewType.checkBox -> { + val binding = ItemSourceEditCheckBoxBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + CheckBoxViewHolder(binding) + } + + else -> { + val binding = ItemSourceEditBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + binding.editText.addLegadoPattern() + binding.editText.addJsonPattern() + binding.editText.addJsPattern() + EditTextViewHolder(binding) + } + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is CheckBoxViewHolder -> holder.bind(editEntities[position]) + is EditTextViewHolder -> holder.bind(editEntities[position]) + } + } + + override fun getItemCount(): Int { + return editEntities.size + } + + inner class EditTextViewHolder(val binding: ItemSourceEditBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(editEntity: EditEntity) = binding.run { + editText.maxLines = editEntityMaxLine + if (editText.getTag(R.id.tag1) == null) { + val listener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + editText.isCursorVisible = false + editText.isCursorVisible = true + editText.isFocusable = true + editText.isFocusableInTouchMode = true + } + + override fun onViewDetachedFromWindow(v: View) { + + } + } + editText.addOnAttachStateChangeListener(listener) + editText.setTag(R.id.tag1, listener) + } + editText.getTag(R.id.tag2)?.let { + if (it is TextWatcher) { + editText.removeTextChangedListener(it) + } + } + editText.setText(editEntity.value) + textInputLayout.hint = editEntity.hint + val textWatcher = object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + + } + + override fun afterTextChanged(s: Editable?) { + editEntity.value = (s?.toString()) + } + } + editText.addTextChangedListener(textWatcher) + editText.setTag(R.id.tag2, textWatcher) + } + } + + class CheckBoxViewHolder(val binding: ItemSourceEditCheckBoxBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(editEntity: EditEntity) = binding.run { + checkBox.text = editEntity.hint + checkBox.isChecked = editEntity.value.isTrue() + checkBox.setOnUserCheckedChangeListener { isChecked -> + editEntity.value = isChecked.toString() + } + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt new file mode 100755 index 000000000..a965bdc28 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt @@ -0,0 +1,119 @@ +package io.legado.app.ui.rss.source.edit + +import android.app.Application +import android.content.Intent +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.AppCacheManager +import io.legado.app.help.RuleComplete +import io.legado.app.help.http.CookieStore +import io.legado.app.help.source.removeSortCache +import io.legado.app.model.SharedJsScope +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getClipText +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers + + +class RssSourceEditViewModel(application: Application) : BaseViewModel(application) { + var autoComplete = false + var rssSource: RssSource? = null + + fun initData(intent: Intent, onFinally: () -> Unit) { + execute { + val key = intent.getStringExtra("sourceUrl") + if (key != null) { + appDb.rssSourceDao.getByKey(key)?.let { + rssSource = it + } + } + }.onFinally { + onFinally() + } + } + + fun save(source: RssSource, success: ((RssSource) -> Unit)) { + execute { + if (source.sourceUrl.isBlank() || source.sourceName.isBlank()) { + throw NoStackTraceException(context.getString(R.string.non_null_name_url)) + } + val oldSource = rssSource ?: RssSource() + if (!source.equal(oldSource)) { + source.lastUpdateTime = System.currentTimeMillis() + if (oldSource.sortUrl != source.sortUrl) { + oldSource.removeSortCache() + } + if (oldSource.jsLib != source.jsLib) { + SharedJsScope.remove(oldSource.jsLib) + } + } + rssSource?.let { + appDb.rssSourceDao.delete(it) + //更新收藏的源地址 + if (it.sourceUrl != source.sourceUrl) { + appDb.rssStarDao.updateOrigin(source.sourceUrl, it.sourceUrl) + appDb.rssArticleDao.updateOrigin(source.sourceUrl, it.sourceUrl) + appDb.cacheDao.deleteSourceVariables(it.sourceUrl) + AppCacheManager.clearSourceVariables() + } + } + appDb.rssSourceDao.insert(source) + rssSource = source + source + }.onSuccess { + success(it) + }.onError { + context.toastOnUi(it.localizedMessage) + it.printOnDebug() + } + } + + fun pasteSource(onSuccess: (source: RssSource) -> Unit) { + execute(context = Dispatchers.Main) { + var source: RssSource? = null + context.getClipText()?.let { json -> + source = GSON.fromJsonObject(json).getOrThrow() + } + source + }.onError { + context.toastOnUi(it.localizedMessage) + }.onSuccess { + if (it != null) { + onSuccess(it) + } else { + context.toastOnUi("格式不对") + } + } + } + + fun importSource(text: String, finally: (source: RssSource) -> Unit) { + execute { + val text1 = text.trim() + GSON.fromJsonObject(text1).getOrThrow().let { + finally.invoke(it) + } + }.onError { + context.toastOnUi(it.stackTraceStr) + } + } + + fun clearCookie(url: String) { + execute { + CookieStore.removeCookie(url) + } + } + + fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? { + if (autoComplete) { + return RuleComplete.autoComplete(rule, preRule, type) + } + return rule + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt new file mode 100755 index 000000000..415481892 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt @@ -0,0 +1,148 @@ +package io.legado.app.ui.rss.source.manage + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.appDb +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemGroupManageBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.requestInputMethod +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch + + +class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val viewModel: RssSourceViewModel by activityViewModels() + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { GroupAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { + toolBar.setBackgroundColor(primaryColor) + toolBar.title = getString(R.string.group_manage) + toolBar.inflateMenu(R.menu.group_manage) + toolBar.menu.applyTint(requireContext()) + toolBar.setOnMenuItemClickListener(this@GroupManageDialog) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.addItemDecoration(VerticalDivider(requireContext())) + recyclerView.adapter = adapter + tvOk.setTextColor(requireContext().accentColor) + tvOk.visible() + tvOk.setOnClickListener { + dismissAllowingStateLoss() + } + initData() + } + + private fun initData() { + lifecycleScope.launch { + appDb.rssSourceDao.flowGroups().conflate().collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> addGroup() + } + return true + } + + @SuppressLint("InflateParams") + private fun addGroup() { + alert(title = getString(R.string.add_group)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotBlank()) { + viewModel.addGroup(it) + } + } + } + cancelButton() + }.requestInputMethod() + } + + @SuppressLint("InflateParams") + private fun editGroup(group: String) { + alert(title = getString(R.string.group_edit)) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setText(group) + } + customView { alertBinding.root } + okButton { + viewModel.upGroup(group, alertBinding.editView.text?.toString()) + } + cancelButton() + }.requestInputMethod() + } + + private inner class GroupAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding { + return ItemGroupManageBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemGroupManageBinding, + item: String, + payloads: MutableList + ) { + binding.run { + root.setBackgroundColor(context.backgroundColor) + tvGroup.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) { + binding.apply { + tvEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + + tvDel.setOnClickListener { + getItem(holder.layoutPosition)?.let { + viewModel.delGroup(it) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt new file mode 100755 index 000000000..c36f9eed8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt @@ -0,0 +1,414 @@ +package io.legado.app.ui.rss.source.manage + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import androidx.activity.viewModels +import androidx.appcompat.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ActivityRssSourceBinding +import io.legado.app.databinding.DialogEditTextBinding +import io.legado.app.help.DirectLinkUpload +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.lib.theme.primaryTextColor +import io.legado.app.ui.association.ImportRssSourceDialog +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.qrcode.QrCodeResult +import io.legado.app.ui.rss.source.edit.RssSourceEditActivity +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.ACache +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.launch +import io.legado.app.utils.sendToClip +import io.legado.app.utils.setEdgeEffectColor +import io.legado.app.utils.share +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.showHelp +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.startActivity +import io.legado.app.utils.transaction +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 订阅源管理 + */ +class RssSourceActivity : VMBaseActivity(), + PopupMenu.OnMenuItemClickListener, + SelectActionBar.CallBack, + RssSourceAdapter.CallBack { + + override val binding by viewBinding(ActivityRssSourceBinding::inflate) + override val viewModel by viewModels() + private val importRecordKey = "rssSourceRecordKey" + private val adapter by lazy { RssSourceAdapter(this, this) } + private val searchView: SearchView by lazy { + binding.titleBar.findViewById(R.id.search_view) + } + private var sourceFlowJob: Job? = null + private var groups = arrayListOf() + private var groupMenu: SubMenu? = null + private val qrCodeResult = registerForActivityResult(QrCodeResult()) { + it ?: return@registerForActivityResult + showDialogFragment(ImportRssSourceDialog(it)) + } + private val importDoc = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + showDialogFragment(ImportRssSourceDialog(uri.toString())) + } + } + private val exportResult = registerForActivityResult(HandleFileContract()) { + it.uri?.let { uri -> + alert(R.string.export_success) { + if (uri.toString().isAbsUrl()) { + setMessage(DirectLinkUpload.getSummary()) + } + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = getString(R.string.path) + editView.setText(uri.toString()) + } + customView { alertBinding.root } + okButton { + sendToClip(uri.toString()) + } + } + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initRecyclerView() + initSearchView() + initGroupFlow() + upSourceFlow() + initSelectActionBar() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_source, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + groupMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupMenu() + return super.onPrepareOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add -> startActivity() + R.id.menu_import_local -> importDoc.launch { + mode = HandleFileContract.FILE + allowExtensions = arrayOf("txt", "json") + } + + R.id.menu_import_onLine -> showImportDialog() + R.id.menu_import_qr -> qrCodeResult.launch() + R.id.menu_group_manage -> showDialogFragment() + R.id.menu_import_default -> viewModel.importDefault() + R.id.menu_enabled_group -> { + searchView.setQuery(getString(R.string.enabled), true) + } + + R.id.menu_disabled_group -> { + searchView.setQuery(getString(R.string.disabled), true) + } + + R.id.menu_group_login -> { + searchView.setQuery(getString(R.string.need_login), true) + } + + R.id.menu_group_null -> { + searchView.setQuery(getString(R.string.no_group), true) + } + + R.id.menu_help -> showHelp("SourceMRssHelp") + else -> if (item.groupId == R.id.source_group) { + searchView.setQuery("group:${item.title}", true) + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection) + R.id.menu_add_group -> selectionAddToGroups() + R.id.menu_remove_group -> selectionRemoveFromGroups() + R.id.menu_top_sel -> viewModel.topSource(*adapter.selection.toTypedArray()) + R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray()) + R.id.menu_export_selection -> viewModel.saveToFile(adapter.selection) { file -> + exportResult.launch { + mode = HandleFileContract.EXPORT + fileData = HandleFileContract.FileData( + "exportRssSource.json", file, "application/json" + ) + } + } + + R.id.menu_share_source -> viewModel.saveToFile(adapter.selection) { + share(it) + } + + R.id.menu_check_selected_interval -> adapter.checkSelectedInterval() + } + return true + } + + private fun initRecyclerView() { + binding.recyclerView.setEdgeEffectColor(primaryColor) + binding.recyclerView.addItemDecoration(VerticalDivider(this)) + binding.recyclerView.adapter = adapter + // When this page is opened, it is in selection mode + val dragSelectTouchHelper: DragSelectTouchHelper = + DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50) + dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView) + dragSelectTouchHelper.activeSlideSelect() + // Note: need judge selection first, so add ItemTouchHelper after it. + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initSearchView() { + binding.titleBar.findViewById(R.id.search_view).let { + it.applyTint(primaryTextColor) + it.onActionViewExpanded() + it.queryHint = getString(R.string.search_rss_source) + it.clearFocus() + it.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + upSourceFlow(newText) + return false + } + }) + } + } + + private fun initSelectActionBar() { + binding.selectActionBar.setMainActionText(R.string.delete) + binding.selectActionBar.inflateMenu(R.menu.rss_source_sel) + binding.selectActionBar.setOnMenuItemClickListener(this) + binding.selectActionBar.setCallBack(this) + } + + private fun initGroupFlow() { + lifecycleScope.launch { + appDb.rssSourceDao.flowGroups().conflate().collect { + groups.clear() + groups.addAll(it) + upGroupMenu() + } + } + } + + @SuppressLint("InflateParams") + private fun selectionAddToGroups() { + alert(titleResource = R.string.add_group) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotEmpty()) { + viewModel.selectionAddToGroups(adapter.selection, it) + } + } + } + cancelButton() + } + } + + @SuppressLint("InflateParams") + private fun selectionRemoveFromGroups() { + alert(titleResource = R.string.remove_group) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.setHint(R.string.group_name) + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dpToPx() + } + customView { alertBinding.root } + okButton { + alertBinding.editView.text?.toString()?.let { + if (it.isNotEmpty()) { + viewModel.selectionRemoveFromGroups(adapter.selection, it) + } + } + } + cancelButton() + } + } + + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickSelectBarMainAction() { + delSourceDialog() + } + + private fun delSourceDialog() { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + yesButton { viewModel.del(*adapter.selection.toTypedArray()) } + noButton() + } + } + + private fun upGroupMenu() = groupMenu?.transaction { menu -> + menu.removeGroup(R.id.source_group) + groups.forEach { + menu.add(R.id.source_group, Menu.NONE, Menu.NONE, it) + } + } + + private fun upSourceFlow(searchKey: String? = null) { + sourceFlowJob?.cancel() + sourceFlowJob = lifecycleScope.launch { + when { + searchKey.isNullOrBlank() -> { + appDb.rssSourceDao.flowAll() + } + + searchKey == getString(R.string.enabled) -> { + appDb.rssSourceDao.flowEnabled() + } + + searchKey == getString(R.string.disabled) -> { + appDb.rssSourceDao.flowDisabled() + } + + searchKey == getString(R.string.need_login) -> { + appDb.rssSourceDao.flowLogin() + } + + searchKey == getString(R.string.no_group) -> { + appDb.rssSourceDao.flowNoGroup() + } + + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + appDb.rssSourceDao.flowGroupSearch(key) + } + + else -> { + appDb.rssSourceDao.flowSearch(searchKey) + } + }.catch { + AppLog.put("订阅源管理界面更新数据出错", it) + }.flowOn(IO).conflate().collect { + adapter.setItems(it, adapter.diffItemCallback) + delay(100) + } + } + } + + override fun upCountView() { + binding.selectActionBar.upCountView( + adapter.selection.size, + adapter.itemCount + ) + } + + @SuppressLint("InflateParams") + private fun showImportDialog() { + val aCache = ACache.get(cacheDir = false) + val cacheUrls: MutableList = aCache + .getAsString(importRecordKey) + ?.splitNotBlank(",") + ?.toMutableList() ?: mutableListOf() + alert(titleResource = R.string.import_on_line) { + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + editView.hint = "url" + editView.setFilterValues(cacheUrls) + editView.delCallBack = { + cacheUrls.remove(it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + } + customView { alertBinding.root } + okButton { + val text = alertBinding.editView.text?.toString() + text?.let { + if (it.isAbsUrl() && !cacheUrls.contains(it)) { + cacheUrls.add(0, it) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) + } + showDialogFragment( + ImportRssSourceDialog(it) + ) + } + } + cancelButton() + } + } + + override fun del(source: RssSource) { + alert(R.string.draw) { + setMessage(getString(R.string.sure_del) + "\n" + source.sourceName) + noButton() + yesButton { + viewModel.del(source) + } + } + } + + override fun edit(source: RssSource) { + startActivity { + putExtra("sourceUrl", source.sourceUrl) + } + } + + override fun update(vararg source: RssSource) { + viewModel.update(*source) + } + + override fun toTop(source: RssSource) { + viewModel.topSource(source) + } + + override fun toBottom(source: RssSource) { + viewModel.bottomSource(source) + } + + override fun upOrder() { + viewModel.upOrder() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt new file mode 100755 index 000000000..5014001d0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt @@ -0,0 +1,247 @@ +package io.legado.app.ui.rss.source.manage + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.data.entities.RssSource +import io.legado.app.databinding.ItemRssSourceBinding +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.ColorUtils +import java.util.Collections + + +class RssSourceAdapter(context: Context, val callBack: CallBack) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private val selected = linkedSetOf() + + val selection: List + get() { + return getItems().filter { + selected.contains(it) + } + } + + val diffItemCallback = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: RssSource, newItem: RssSource): Boolean { + return oldItem.sourceUrl == newItem.sourceUrl + } + + override fun areContentsTheSame(oldItem: RssSource, newItem: RssSource): Boolean { + return oldItem.sourceName == newItem.sourceName + && oldItem.sourceGroup == newItem.sourceGroup + && oldItem.enabled == newItem.enabled + } + + override fun getChangePayload(oldItem: RssSource, newItem: RssSource): Any? { + val payload = Bundle() + if (oldItem.sourceName != newItem.sourceName + || oldItem.sourceGroup != newItem.sourceGroup + ) { + payload.putBoolean("upName", true) + } + if (oldItem.enabled != newItem.enabled) { + payload.putBoolean("enabled", newItem.enabled) + } + if (payload.isEmpty) { + return null + } + return payload + } + } + + override fun getViewBinding(parent: ViewGroup): ItemRssSourceBinding { + return ItemRssSourceBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemRssSourceBinding, + item: RssSource, + payloads: MutableList + ) { + binding.run { + if (payloads.isEmpty()) { + root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f)) + cbSource.text = item.getDisplayNameGroup() + swtEnabled.isChecked = item.enabled + cbSource.isChecked = selected.contains(item) + } else { + for (i in payloads.indices) { + val bundle = payloads[i] as Bundle + bundle.keySet().forEach { + when (it) { + "upName" -> cbSource.text = item.getDisplayNameGroup() + "enabled" -> swtEnabled.isChecked = bundle.getBoolean("enabled") + "selected" -> cbSource.isChecked = selected.contains(item) + } + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRssSourceBinding) { + binding.apply { + swtEnabled.setOnUserCheckedChangeListener { checked -> + getItem(holder.layoutPosition)?.let { + it.enabled = checked + callBack.update(it) + } + } + cbSource.setOnUserCheckedChangeListener { checked -> + getItem(holder.layoutPosition)?.let { + if (checked) { + selected.add(it) + } else { + selected.remove(it) + } + callBack.upCountView() + } + } + ivEdit.setOnClickListener { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) + } + } + ivMenuMore.setOnClickListener { + showMenu(ivMenuMore, holder.layoutPosition) + } + } + } + + override fun onCurrentListChanged() { + callBack.upCountView() + } + + fun selectAll() { + getItems().forEach { + selected.add(it) + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (selected.contains(it)) { + selected.remove(it) + } else { + selected.add(it) + } + } + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + fun checkSelectedInterval() { + val selectedPosition = linkedSetOf() + getItems().forEachIndexed { index, it -> + if (selected.contains(it)) { + selectedPosition.add(index) + } + } + val minPosition = Collections.min(selectedPosition) + val maxPosition = Collections.max(selectedPosition) + val itemCount = maxPosition - minPosition + 1 + for (i in minPosition..maxPosition) { + getItem(i)?.let { + selected.add(it) + } + } + notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() + } + + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.rss_source_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(source) + R.id.menu_bottom -> callBack.toBottom(source) + R.id.menu_del -> { + callBack.del(source) + selected.remove(source) + } + } + true + } + popupMenu.show() + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.customOrder == targetItem.customOrder) { + callBack.upOrder() + } else { + val srcOrder = srcItem.customOrder + srcItem.customOrder = targetItem.customOrder + targetItem.customOrder = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = hashSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() + } + } + + val dragSelectCallback: DragSelectTouchHelper.Callback = + object : DragSelectTouchHelper.AdvanceCallback(Mode.ToggleAndReverse) { + override fun currentSelectedId(): MutableSet { + return selected + } + + override fun getItemId(position: Int): RssSource { + return getItem(position)!! + } + + override fun updateSelectState(position: Int, isSelected: Boolean): Boolean { + getItem(position)?.let { + if (isSelected) { + selected.add(it) + } else { + selected.remove(it) + } + notifyItemChanged(position, bundleOf(Pair("selected", null))) + callBack.upCountView() + return true + } + return false + } + } + + interface CallBack { + fun del(source: RssSource) + fun edit(source: RssSource) + fun update(vararg source: RssSource) + fun toTop(source: RssSource) + fun toBottom(source: RssSource) + fun upOrder() + fun upCountView() + } +} diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt new file mode 100755 index 000000000..3158e4535 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt @@ -0,0 +1,168 @@ +package io.legado.app.ui.rss.source.manage + +import android.app.Application +import android.text.TextUtils +import io.legado.app.base.BaseViewModel +import io.legado.app.data.appDb +import io.legado.app.data.entities.RssSource +import io.legado.app.help.DefaultData +import io.legado.app.help.source.SourceHelp +import io.legado.app.utils.FileUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import java.io.File + +/** + * 订阅源管理数据修改 + * 修改数据要copy,直接修改会导致界面不刷新 + */ +class RssSourceViewModel(application: Application) : BaseViewModel(application) { + + fun topSource(vararg sources: RssSource) { + execute { + sources.sortBy { it.customOrder } + val minOrder = appDb.rssSourceDao.minOrder - 1 + val array = Array(sources.size) { + sources[it].copy(customOrder = minOrder - it) + } + appDb.rssSourceDao.update(*array) + } + } + + fun bottomSource(vararg sources: RssSource) { + execute { + sources.sortBy { it.customOrder } + val maxOrder = appDb.rssSourceDao.maxOrder + 1 + val array = Array(sources.size) { + sources[it].copy(customOrder = maxOrder + it) + } + appDb.rssSourceDao.update(*array) + } + } + + fun del(vararg rssSource: RssSource) { + execute { + SourceHelp.deleteRssSources(rssSource.toList()) + } + } + + fun update(vararg rssSource: RssSource) { + execute { appDb.rssSourceDao.update(*rssSource) } + } + + fun upOrder() { + execute { + val sources = appDb.rssSourceDao.all + for ((index: Int, source: RssSource) in sources.withIndex()) { + source.customOrder = index + 1 + } + appDb.rssSourceDao.update(*sources.toTypedArray()) + } + } + + fun enableSelection(sources: List) { + execute { + val array = Array(sources.size) { + sources[it].copy(enabled = true) + } + appDb.rssSourceDao.update(*array) + } + } + + fun disableSelection(sources: List) { + execute { + val array = Array(sources.size) { + sources[it].copy(enabled = false) + } + appDb.rssSourceDao.update(*array) + } + } + + fun saveToFile(sources: List, success: (file: File) -> Unit) { + execute { + val path = "${context.filesDir}/shareRssSource.json" + FileUtils.delete(path) + val file = FileUtils.createFileWithReplace(path) + file.writeText(GSON.toJson(sources)) + file + }.onSuccess { + success.invoke(it) + }.onError { + context.toastOnUi(it.stackTraceStr) + } + } + + fun selectionAddToGroups(sources: List, groups: String) { + execute { + val array = Array(sources.size) { + sources[it].copy().addGroup(groups) + } + appDb.rssSourceDao.update(*array) + } + } + + fun selectionRemoveFromGroups(sources: List, groups: String) { + execute { + val array = Array(sources.size) { + sources[it].copy().removeGroup(groups) + } + appDb.rssSourceDao.update(*array) + } + } + + fun addGroup(group: String) { + execute { + val sources = appDb.rssSourceDao.noGroup + sources.forEach { source -> + source.sourceGroup = group + } + appDb.rssSourceDao.update(*sources.toTypedArray()) + } + } + + fun upGroup(oldGroup: String, newGroup: String?) { + execute { + val sources = appDb.rssSourceDao.getByGroup(oldGroup) + sources.forEach { source -> + source.sourceGroup?.splitNotBlank(",")?.toHashSet()?.let { + it.remove(oldGroup) + if (!newGroup.isNullOrEmpty()) + it.add(newGroup) + source.sourceGroup = TextUtils.join(",", it) + } + } + appDb.rssSourceDao.update(*sources.toTypedArray()) + } + } + + fun delGroup(group: String) { + execute { + execute { + val sources = appDb.rssSourceDao.getByGroup(group) + sources.forEach { source -> + source.sourceGroup?.splitNotBlank(",")?.toHashSet()?.let { + it.remove(group) + source.sourceGroup = TextUtils.join(",", it) + } + } + appDb.rssSourceDao.update(*sources.toTypedArray()) + } + } + } + + fun importDefault() { + execute { + DefaultData.importDefaultRssSources() + } + } + + fun disable(rssSource: RssSource) { + execute { + rssSource.enabled = false + appDb.rssSourceDao.update(rssSource) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt new file mode 100755 index 000000000..f0e84e571 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt @@ -0,0 +1,148 @@ +package io.legado.app.ui.rss.subscription + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.core.view.isGone +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.RuleSub +import io.legado.app.databinding.ActivityRuleSubBinding +import io.legado.app.databinding.DialogRuleSubEditBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.association.ImportBookSourceDialog +import io.legado.app.ui.association.ImportReplaceRuleDialog +import io.legado.app.ui.association.ImportRssSourceDialog +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.toastOnUi +import io.legado.app.utils.viewbindingdelegate.viewBinding +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 规则订阅界面 + */ +class RuleSubActivity : BaseActivity(), + RuleSubAdapter.Callback { + + override val binding by viewBinding(ActivityRuleSubBinding::inflate) + private val adapter by lazy { RuleSubAdapter(this, this) } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.source_subscription, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add -> { + val order = appDb.ruleSubDao.maxOrder + 1 + editSubscription(RuleSub(customOrder = order)) + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun initView() { + binding.recyclerView.adapter = adapter + binding.recyclerView.applyNavigationBarPadding() + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + } + + private fun initData() { + lifecycleScope.launch { + appDb.ruleSubDao.flowAll().catch { + AppLog.put("规则订阅界面获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).conflate().collect { + binding.tvEmptyMsg.isGone = it.isNotEmpty() + adapter.setItems(it) + } + } + } + + override fun openSubscription(ruleSub: RuleSub) { + when (ruleSub.type) { + 0 -> showDialogFragment( + ImportBookSourceDialog(ruleSub.url) + ) + 1 -> showDialogFragment( + ImportRssSourceDialog(ruleSub.url) + ) + 2 -> showDialogFragment( + ImportReplaceRuleDialog(ruleSub.url) + ) + } + } + + override fun editSubscription(ruleSub: RuleSub) { + alert(R.string.rule_subscription) { + val alertBinding = DialogRuleSubEditBinding.inflate(layoutInflater).apply { + if (ruleSub.type !in 0..(context), + ItemTouchCallback.Callback { + + private val typeArray = context.resources.getStringArray(R.array.rule_type) + + override fun convert( + holder: ItemViewHolder, + binding: ItemRuleSubBinding, + item: RuleSub, + payloads: MutableList + ) { + binding.tvType.text = typeArray[item.type] + binding.tvName.text = item.name + binding.tvUrl.text = item.url + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemRuleSubBinding) { + binding.root.setOnClickListener { + callBack.openSubscription(getItem(holder.layoutPosition)!!) + } + binding.ivEdit.setOnClickListener { + callBack.editSubscription(getItem(holder.layoutPosition)!!) + } + binding.ivMenuMore.setOnClickListener { + showMenu(binding.ivMenuMore, holder.layoutPosition) + } + } + + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.source_sub_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_del -> callBack.delSubscription(source) + } + true + } + popupMenu.show() + } + + override fun getViewBinding(parent: ViewGroup): ItemRuleSubBinding { + return ItemRuleSubBinding.inflate(inflater, parent, false) + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.customOrder == targetItem.customOrder) { + callBack.upOrder() + } else { + val srcOrder = srcItem.customOrder + srcItem.customOrder = targetItem.customOrder + targetItem.customOrder = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + swapItem(srcPosition, targetPosition) + return true + } + + private val movedItems = hashSetOf() + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.updateSourceSub(*movedItems.toTypedArray()) + movedItems.clear() + } + } + + interface Callback { + fun openSubscription(ruleSub: RuleSub) + fun editSubscription(ruleSub: RuleSub) + fun delSubscription(ruleSub: RuleSub) + fun updateSourceSub(vararg ruleSub: RuleSub) + fun upOrder() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt b/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt new file mode 100755 index 000000000..ed0b97220 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt @@ -0,0 +1,95 @@ +package io.legado.app.ui.welcome + +import android.content.Intent +import android.graphics.drawable.BitmapDrawable +import android.os.Bundle +import androidx.core.view.postDelayed +import io.legado.app.base.BaseActivity +import io.legado.app.constant.PreferKey +import io.legado.app.constant.Theme +import io.legado.app.data.appDb +import io.legado.app.databinding.ActivityWelcomeBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ThemeConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.main.MainActivity +import io.legado.app.utils.BitmapUtils +import io.legado.app.utils.fullScreen +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefString +import io.legado.app.utils.setStatusBarColorAuto +import io.legado.app.utils.startActivity +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import io.legado.app.utils.windowSize + +open class WelcomeActivity : BaseActivity() { + + override val binding by viewBinding(ActivityWelcomeBinding::inflate) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + binding.ivBook.setColorFilter(accentColor) + binding.vwTitleLine.setBackgroundColor(accentColor) + // 避免从桌面启动程序后,会重新实例化入口类的activity + if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) { + finish() + } else { + binding.root.postDelayed(600) { startMainActivity() } + } + } + + override fun setupSystemBar() { + fullScreen() + setStatusBarColorAuto(backgroundColor, true, fullScreen) + upNavigationBarColor() + } + + override fun upBackgroundImage() { + if (getPrefBoolean(PreferKey.customWelcome)) { + kotlin.runCatching { + when (ThemeConfig.getTheme()) { + Theme.Dark -> getPrefString(PreferKey.welcomeImageDark)?.let { path -> + val size = windowManager.windowSize + BitmapUtils.decodeBitmap(path, size.widthPixels, size.heightPixels).let { + binding.tvLegado.visible(AppConfig.welcomeShowTextDark) + binding.ivBook.visible(AppConfig.welcomeShowIconDark) + binding.tvGzh.visible(AppConfig.welcomeShowTextDark) + window.decorView.background = BitmapDrawable(resources, it) + return + } + } + + else -> getPrefString(PreferKey.welcomeImage)?.let { path -> + val size = windowManager.windowSize + BitmapUtils.decodeBitmap(path, size.widthPixels, size.heightPixels).let { + binding.tvLegado.visible(AppConfig.welcomeShowText) + binding.ivBook.visible(AppConfig.welcomeShowIcon) + binding.tvGzh.visible(AppConfig.welcomeShowText) + window.decorView.background = BitmapDrawable(resources, it) + return + } + } + } + } + } + super.upBackgroundImage() + } + + private fun startMainActivity() { + startActivity() + if (getPrefBoolean(PreferKey.defaultToRead) && appDb.bookDao.lastReadBook != null) { + startActivity() + } + finish() + } + +} + +class Launcher1 : WelcomeActivity() +class Launcher2 : WelcomeActivity() +class Launcher3 : WelcomeActivity() +class Launcher4 : WelcomeActivity() +class Launcher5 : WelcomeActivity() +class Launcher6 : WelcomeActivity() \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt new file mode 100755 index 000000000..1ff8f6fa3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt @@ -0,0 +1,122 @@ +package io.legado.app.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface +import android.os.Build +import android.text.StaticLayout +import android.util.AttributeSet +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory +import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw +import io.legado.app.utils.dpToPx + +class BatteryView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + private val batteryTypeface by lazy { + Typeface.createFromAsset(context.assets, "font/number.ttf") + } + private val batteryPaint = Paint() + private val outFrame = Rect() + private val polar = Rect() + private val canvasRecorder = CanvasRecorderFactory.create() + var isBattery = false + set(value) { + field = value + if (value && !isInEditMode) { + super.setTypeface(batteryTypeface) + postInvalidate() + } + } + private var battery: Int = 0 + + init { + setPadding(4.dpToPx(), 3.dpToPx(), 6.dpToPx(), 3.dpToPx()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isFallbackLineSpacing = false + } + batteryPaint.strokeWidth = 1f.dpToPx() + batteryPaint.isAntiAlias = true + batteryPaint.color = paint.color + } + + override fun setTypeface(tf: Typeface?) { + if (!isBattery) { + super.setTypeface(tf) + } + } + + fun setColor(@ColorInt color: Int) { + setTextColor(color) + batteryPaint.color = color + invalidate() + } + + @SuppressLint("SetTextI18n") + fun setBattery(battery: Int, text: String? = null) { + this.battery = battery + if (text.isNullOrEmpty()) { + setText(battery.toString()) + } else { + setText("$text $battery") + } + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + canvasRecorder.invalidate() + } + + override fun onDraw(canvas: Canvas) { + if (AppConfig.optimizeRender) { + canvasRecorder.recordIfNeededThenDraw(canvas, width, height) { + super.onDraw(this) + drawBattery(this) + } + } else { + super.onDraw(canvas) + drawBattery(canvas) + } + } + + private fun drawBattery(canvas: Canvas) { + if (!isBattery) return + layout.getLineBounds(0, outFrame) + val batteryStart = layout + .getPrimaryHorizontal(text.length - battery.toString().length) + .toInt() + 2.dpToPx() + val batteryEnd = batteryStart + + StaticLayout.getDesiredWidth(battery.toString(), paint).toInt() + 4.dpToPx() + outFrame.set( + batteryStart, + 2.dpToPx(), + batteryEnd, + height - 2.dpToPx() + ) + val dj = (outFrame.bottom - outFrame.top) / 3 + polar.set( + batteryEnd, + outFrame.top + dj, + batteryEnd + 2.dpToPx(), + outFrame.bottom - dj + ) + batteryPaint.style = Paint.Style.STROKE + canvas.drawRect(outFrame, batteryPaint) + batteryPaint.style = Paint.Style.FILL + canvas.drawRect(polar, batteryPaint) + } + + @Suppress("UNNECESSARY_SAFE_CALL") + override fun invalidate() { + super.invalidate() + canvasRecorder?.invalidate() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt new file mode 100755 index 000000000..8b0790ab3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt @@ -0,0 +1,92 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.graphics.PorterDuff +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.FrameLayout +import android.widget.SeekBar +import androidx.appcompat.widget.TooltipCompat +import io.legado.app.R +import io.legado.app.databinding.ViewDetailSeekBarBinding +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.ui.widget.seekbar.SeekBarChangeListener +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.progressAdd + + +class DetailSeekBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs), + SeekBarChangeListener { + private var binding: ViewDetailSeekBarBinding = + ViewDetailSeekBarBinding.inflate(LayoutInflater.from(context), this, true) + private val isBottomBackground: Boolean + + var valueFormat: ((progress: Int) -> String)? = null + var onChanged: ((progress: Int) -> Unit)? = null + var progress: Int + get() = binding.seekBar.progress + set(value) { + binding.seekBar.progress = value + upValue() + } + var max: Int + get() = binding.seekBar.max + set(value) { + binding.seekBar.max = value + } + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.DetailSeekBar) + isBottomBackground = + typedArray.getBoolean(R.styleable.DetailSeekBar_isBottomBackground, false) + val title = typedArray.getText(R.styleable.DetailSeekBar_title) + binding.tvSeekTitle.apply { + text = title + TooltipCompat.setTooltipText(this, title) + } + binding.seekBar.max = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0) + typedArray.recycle() + if (isBottomBackground && !isInEditMode) { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val textColor = context.getPrimaryTextColor(isLight) + binding.tvSeekTitle.setTextColor(textColor) + binding.ivSeekPlus.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + binding.ivSeekReduce.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) + binding.tvSeekValue.setTextColor(textColor) + } + binding.ivSeekPlus.setOnClickListener { + binding.seekBar.progressAdd(1) + onChanged?.invoke(binding.seekBar.progress) + } + binding.ivSeekReduce.setOnClickListener { + binding.seekBar.progressAdd(-1) + onChanged?.invoke(binding.seekBar.progress) + } + binding.seekBar.setOnSeekBarChangeListener(this) + } + + private fun upValue(progress: Int = binding.seekBar.progress) { + valueFormat?.let { + binding.tvSeekValue.text = it.invoke(progress) + } ?: let { + binding.tvSeekValue.text = progress.toString() + } + } + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + upValue(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + onChanged?.invoke(binding.seekBar.progress) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt b/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt new file mode 100755 index 000000000..0c50e0434 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt @@ -0,0 +1,62 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import android.widget.TextView +import io.legado.app.ui.widget.text.AccentBgTextView +import io.legado.app.utils.dpToPx + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class LabelsBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { + + private val unUsedViews = arrayListOf() + private val usedViews = arrayListOf() + var textSize = 12f + + fun setLabels(labels: Array) { + clear() + labels.forEach { + addLabel(it) + } + } + + fun setLabels(labels: List) { + clear() + labels.forEach { + addLabel(it) + } + } + + fun clear() { + unUsedViews.addAll(usedViews) + usedViews.clear() + removeAllViews() + } + + fun addLabel(label: String) { + val tv = if (unUsedViews.isEmpty()) { + AccentBgTextView(context, null).apply { + setPadding(3.dpToPx(), 0, 3.dpToPx(), 0) + setRadius(2) + val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + lp.setMargins(0, 0, 2.dpToPx(), 0) + layoutParams = lp + text = label + maxLines = 1 + usedViews.add(this) + } + } else { + unUsedViews.last().apply { + usedViews.add(this) + unUsedViews.removeAt(unUsedViews.lastIndex) + } + } + tv.textSize = textSize + tv.text = label + addView(tv) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/NoChildScrollNestedScrollView.kt b/app/src/main/java/io/legado/app/ui/widget/NoChildScrollNestedScrollView.kt new file mode 100755 index 000000000..b518eb812 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/NoChildScrollNestedScrollView.kt @@ -0,0 +1,29 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import androidx.core.widget.NestedScrollView + +class NoChildScrollNestedScrollView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : NestedScrollView(context, attrs) { + + private var isRequestChildFocus = false + + override fun requestChildFocus(child: View?, focused: View?) { + isRequestChildFocus = true + super.requestChildFocus(child, focused) + isRequestChildFocus = false + } + + override fun computeScrollDeltaToGetChildRectOnScreen(rect: Rect?): Int { + if (isRequestChildFocus) { + return 0 + } + return super.computeScrollDeltaToGetChildRectOnScreen(rect) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/PopupAction.kt b/app/src/main/java/io/legado/app/ui/widget/PopupAction.kt new file mode 100755 index 000000000..bf2d54c6a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/PopupAction.kt @@ -0,0 +1,69 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.view.ViewGroup +import android.widget.PopupWindow +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.ItemTextBinding +import io.legado.app.databinding.PopupActionBinding +import io.legado.app.lib.dialogs.SelectItem +import splitties.systemservices.layoutInflater + +class PopupAction(private val context: Context) : + PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) { + + val binding = PopupActionBinding.inflate(context.layoutInflater) + val adapter by lazy { + Adapter(context).apply { + setHasStableIds(true) + } + } + var onActionClick: ((action: String) -> Unit)? = null + + init { + contentView = binding.root + + isTouchable = true + isOutsideTouchable = false + isFocusable = true + + binding.recyclerView.adapter = adapter + } + + fun setItems(items: List>) { + adapter.setItems(items) + } + + inner class Adapter(context: Context) : + RecyclerAdapter, ItemTextBinding>(context) { + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getViewBinding(parent: ViewGroup): ItemTextBinding { + return ItemTextBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemTextBinding, + item: SelectItem, + payloads: MutableList + ) { + with(binding) { + textView.text = item.title + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemTextBinding) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { item -> + onActionClick?.invoke(item.value) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/ReaderInfoBarView.kt b/app/src/main/java/io/legado/app/ui/widget/ReaderInfoBarView.kt new file mode 100755 index 000000000..fdd3758bc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/ReaderInfoBarView.kt @@ -0,0 +1,194 @@ +package io.legado.app.ui.widget + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import androidx.annotation.AttrRes +import androidx.core.content.ContextCompat +import androidx.core.content.res.use +import androidx.core.graphics.ColorUtils +import androidx.core.view.WindowInsetsCompat +import io.legado.app.utils.dpToPx +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import java.text.SimpleDateFormat +import java.util.Date +import kotlin.math.min +import com.google.android.material.R as materialR + +class ReaderInfoBarView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : View(context, attrs, defStyleAttr) { + + companion object { + const val ALIGN_LEFT = 0 + const val ALIGN_CENTER = 1 + } + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val textBounds = Rect() + private val timeFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT) + private val timeReceiver = TimeReceiver() + private var insetLeft: Int = 0 + private var insetRight: Int = 0 + private var insetTop: Int = 0 + private var cutoutInsetLeft = 0 + private var cutoutInsetRight = 0 + private val colorText = ColorUtils.setAlphaComponent( + context.obtainStyledAttributes(intArrayOf(materialR.attr.colorOnSurface)).use { + it.getColor(0, Color.BLACK) + }, + 200, + ) + private val colorOutline = ColorUtils.setAlphaComponent( + context.obtainStyledAttributes(intArrayOf(materialR.attr.colorSurface)).use { + it.getColor(0, Color.WHITE) + }, + 200, + ) + + var textInfoAlignment: Int = ALIGN_CENTER + set(value) { + field = value + updateTextSize() + invalidate() + } + private var timeText = timeFormat.format(Date()) + private var text: String = "" + private val innerHeight + get() = height - paddingTop - paddingBottom - insetTop + private val innerWidth + get() = width - paddingLeft - paddingRight - insetLeft - insetRight + + init { + val insetStart = 10.dpToPx() + val insetEnd = 10.dpToPx() + paint.strokeWidth = 2f.dpToPx() + paint.setShadowLayer(2f, 1f, 1f, Color.GRAY) + insetLeft = insetStart + insetRight = insetEnd + insetTop = minOf(insetLeft, insetRight) + setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + if (insets.left >= paddingLeft) { + cutoutInsetLeft = insets.left + } + if (insets.right >= paddingRight) { + cutoutInsetRight = insets.right + } + windowInsets + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom + + val textX = when (textInfoAlignment) { + ALIGN_CENTER -> { + val textWidth = paint.measureText(text) + (width / 2f).coerceIn( + paddingLeft + insetLeft + cutoutInsetLeft + textWidth / 2, + width - paddingRight - insetRight - cutoutInsetRight - textWidth / 2 + ) + } + + else -> (paddingLeft + insetLeft + cutoutInsetLeft).toFloat() + } + paint.textAlign = when (textInfoAlignment) { + ALIGN_CENTER -> Paint.Align.CENTER + else -> Paint.Align.LEFT + } + + canvas.drawTextOutline( + text, + textX, + paddingTop + insetTop + ty, + ) + + paint.textAlign = Paint.Align.RIGHT + canvas.drawTextOutline( + timeText, + (width - paddingRight - insetRight - cutoutInsetRight).toFloat(), + paddingTop + insetTop + ty, + ) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + updateTextSize() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + ContextCompat.registerReceiver( + context, + timeReceiver, + IntentFilter(Intent.ACTION_TIME_TICK), + ContextCompat.RECEIVER_EXPORTED, + ) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + context.unregisterReceiver(timeReceiver) + } + + fun update(label: String) { + text = label + updateTextSize() + invalidate() + } + + private fun updateTextSize() { + val testTextSize = 48f + paint.textSize = testTextSize + paint.getTextBounds(text, 0, text.length, textBounds) + + val maxTextHeight = innerHeight * 0.8f + val scaleFactor = min( + maxTextHeight / textBounds.height(), + calculateMaxWidthScale() + ) + paint.textSize = testTextSize * scaleFactor + + paint.getTextBounds(text, 0, text.length, textBounds) + } + + private fun calculateMaxWidthScale(): Float { + return when (textInfoAlignment) { + ALIGN_CENTER -> { + val availableWidth = innerWidth - cutoutInsetLeft - cutoutInsetRight + val requiredWidth = paint.measureText(text) + if (requiredWidth > availableWidth) availableWidth / requiredWidth else 1f + } + + else -> 1f + } + } + + private fun Canvas.drawTextOutline(text: String, x: Float, y: Float) { + paint.color = colorOutline + paint.style = Paint.Style.STROKE + drawText(text, x, y, paint) + paint.color = colorText + paint.style = Paint.Style.FILL + drawText(text, x, y, paint) + } + + private inner class TimeReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context?, intent: Intent?) { + timeText = timeFormat.format(Date()) + invalidate() + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/SearchView.kt b/app/src/main/java/io/legado/app/ui/widget/SearchView.kt new file mode 100755 index 000000000..24eeca7d5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/SearchView.kt @@ -0,0 +1,110 @@ +package io.legado.app.ui.widget + +import android.annotation.SuppressLint +import android.app.SearchableInfo +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.os.Build +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.ImageSpan +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.widget.TextView +import androidx.appcompat.widget.SearchView +import io.legado.app.R +import io.legado.app.utils.printOnDebug + + +class SearchView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : SearchView(context, attrs) { + private var mSearchHintIcon: Drawable? = null + private var textView: TextView? = null + + @SuppressLint("UseCompatLoadingForDrawables") + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + super.onLayout(changed, left, top, right, bottom) + try { + if (textView == null) { + textView = findViewById(androidx.appcompat.R.id.search_src_text) + mSearchHintIcon = this.context.getDrawable(R.drawable.ic_search_hint) + } + // 改变字体 + textView!!.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) + textView!!.gravity = Gravity.CENTER_VERTICAL + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + textView!!.isLocalePreferredLineHeightForMinimumUsed = false + } + updateQueryHint() + } catch (e: Exception) { + e.printOnDebug() + } + } + + private fun getDecoratedHint(hintText: CharSequence): CharSequence { + // If the field is always expanded or we don't have a search hint icon, + // then don't add the search icon to the hint. + if (mSearchHintIcon == null) { + return hintText + } + val textSize = textView!!.textSize.toInt() + mSearchHintIcon!!.setBounds(0, 0, textSize, textSize) + val ssb = SpannableStringBuilder(" ") + ssb.setSpan(CenteredImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + ssb.append(hintText) + return ssb + } + + private fun updateQueryHint() { + textView?.let { + it.hint = getDecoratedHint(queryHint ?: "") + } + } + + override fun setIconifiedByDefault(iconified: Boolean) { + super.setIconifiedByDefault(iconified) + updateQueryHint() + } + + override fun setSearchableInfo(searchable: SearchableInfo?) { + super.setSearchableInfo(searchable) + searchable?.let { + updateQueryHint() + } + } + + override fun setQueryHint(hint: CharSequence?) { + super.setQueryHint(hint) + updateQueryHint() + } + + internal class CenteredImageSpan(drawable: Drawable?) : ImageSpan(drawable!!) { + override fun draw( + canvas: Canvas, text: CharSequence, + start: Int, end: Int, x: Float, + top: Int, y: Int, bottom: Int, paint: Paint + ) { + // image to draw + val b = drawable + // font metrics of text to be replaced + val fm = paint.fontMetricsInt + val transY = ((y + fm.descent + y + fm.ascent) / 2 + - b.bounds.bottom / 2) + canvas.save() + canvas.translate(x, transY.toFloat()) + b.draw(canvas) + canvas.restore() + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt b/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt new file mode 100755 index 000000000..97ff40f5c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt @@ -0,0 +1,128 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.graphics.PorterDuff +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.Menu +import android.widget.FrameLayout +import androidx.annotation.MenuRes +import androidx.annotation.StringRes +import androidx.appcompat.widget.PopupMenu +import io.legado.app.R +import io.legado.app.databinding.ViewSelectActionBarBinding +import io.legado.app.lib.theme.TintHelper +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.elevation +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.getSecondaryDisabledTextColor +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.applyNavigationBarPadding +import io.legado.app.utils.visible + + +@Suppress("unused") +class SelectActionBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + private val bgIsLight = ColorUtils.isColorLight(context.bottomBackground) + private val primaryTextColor = context.getPrimaryTextColor(bgIsLight) + private val disabledColor = context.getSecondaryDisabledTextColor(bgIsLight) + + private var callBack: CallBack? = null + private var selMenu: PopupMenu? = null + private val binding = ViewSelectActionBarBinding + .inflate(LayoutInflater.from(context), this, true) + + init { + if (!isInEditMode) { + setBackgroundColor(context.bottomBackground) + elevation = context.elevation + binding.cbSelectedAll.setTextColor(primaryTextColor) + TintHelper.setTint(binding.cbSelectedAll, context.accentColor, !bgIsLight) + binding.ivMenuMore.setColorFilter(disabledColor, PorterDuff.Mode.SRC_IN) + binding.cbSelectedAll.setOnUserCheckedChangeListener { isChecked -> + callBack?.selectAll(isChecked) + } + binding.btnRevertSelection.setOnClickListener { callBack?.revertSelection() } + binding.btnSelectActionMain.setOnClickListener { callBack?.onClickSelectBarMainAction() } + binding.ivMenuMore.setOnClickListener { selMenu?.show() } + applyNavigationBarPadding() + } + } + + fun setMainActionText(text: String) = binding.run { + btnSelectActionMain.text = text + btnSelectActionMain.visible() + } + + fun setMainActionText(@StringRes id: Int) = binding.run { + btnSelectActionMain.setText(id) + btnSelectActionMain.visible() + } + + fun inflateMenu(@MenuRes resId: Int): Menu? { + selMenu = PopupMenu(context, binding.ivMenuMore) + selMenu?.inflate(resId) + binding.ivMenuMore.visible() + return selMenu?.menu + } + + fun setCallBack(callBack: CallBack) { + this.callBack = callBack + } + + fun setOnMenuItemClickListener(listener: PopupMenu.OnMenuItemClickListener) { + selMenu?.setOnMenuItemClickListener(listener) + } + + fun upCountView(selectCount: Int, allCount: Int) = binding.run { + if (selectCount == 0) { + cbSelectedAll.isChecked = false + } else { + cbSelectedAll.isChecked = selectCount >= allCount + } + + //重置全选的文字 + if (cbSelectedAll.isChecked) { + cbSelectedAll.text = context.getString( + R.string.select_cancel_count, + selectCount, + allCount + ) + } else { + cbSelectedAll.text = context.getString( + R.string.select_all_count, + selectCount, + allCount + ) + } + setMenuClickable(selectCount > 0) + } + + private fun setMenuClickable(isClickable: Boolean) = binding.run { + btnRevertSelection.isEnabled = isClickable + btnRevertSelection.isClickable = isClickable + btnSelectActionMain.isEnabled = isClickable + btnSelectActionMain.isClickable = isClickable + if (isClickable) { + ivMenuMore.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN) + } else { + ivMenuMore.setColorFilter(disabledColor, PorterDuff.Mode.SRC_IN) + } + ivMenuMore.isEnabled = isClickable + ivMenuMore.isClickable = isClickable + } + + interface CallBack { + + fun selectAll(selectAll: Boolean) + + fun revertSelection() + + fun onClickSelectBarMainAction() {} + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt b/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt new file mode 100755 index 000000000..95e54ac63 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/ShadowLayout.kt @@ -0,0 +1,185 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.widget.RelativeLayout +import io.legado.app.R +import io.legado.app.utils.getCompatColor + +/** + * ShadowLayout.java + * + * Created by lijiankun on 17/8/11. + */ +@Suppress("unused") +class ShadowLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : RelativeLayout(context, attrs) { + private val mPaint = + Paint(Paint.ANTI_ALIAS_FLAG) + private val mRectF = RectF() + + /** + * 阴影的颜色 + */ + private var mShadowColor = Color.TRANSPARENT + + /** + * 阴影的大小范围 + */ + private var mShadowRadius = 0f + + /** + * 阴影 x 轴的偏移量 + */ + private var mShadowDx = 0f + + /** + * 阴影 y 轴的偏移量 + */ + private var mShadowDy = 0f + + /** + * 阴影显示的边界 + */ + private var mShadowSide = ALL + + /** + * 阴影的形状,圆形/矩形 + */ + private var mShadowShape = SHAPE_RECTANGLE + + + init { + setLayerType(View.LAYER_TYPE_SOFTWARE, null) // 关闭硬件加速 + setWillNotDraw(false) // 调用此方法后,才会执行 onDraw(Canvas) 方法 + val typedArray = + context.obtainStyledAttributes(attrs, R.styleable.ShadowLayout) + mShadowColor = typedArray.getColor( + R.styleable.ShadowLayout_shadowColor, + context.getCompatColor(android.R.color.black) + ) + mShadowRadius = + typedArray.getDimension(R.styleable.ShadowLayout_shadowRadius, dip2px(0f)) + mShadowDx = typedArray.getDimension(R.styleable.ShadowLayout_shadowDx, dip2px(0f)) + mShadowDy = typedArray.getDimension(R.styleable.ShadowLayout_shadowDy, dip2px(0f)) + mShadowSide = + typedArray.getInt(R.styleable.ShadowLayout_shadowSide, ALL) + mShadowShape = typedArray.getInt( + R.styleable.ShadowLayout_shadowShape, + SHAPE_RECTANGLE + ) + typedArray.recycle() + + setUpShadowPaint() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val effect = mShadowRadius + dip2px(5f) + var rectLeft = 0f + var rectTop = 0f + var rectRight = this.measuredWidth.toFloat() + var rectBottom = this.measuredHeight.toFloat() + var paddingLeft = 0 + var paddingTop = 0 + var paddingRight = 0 + var paddingBottom = 0 + this.width + if (mShadowSide and LEFT == LEFT) { + rectLeft = effect + paddingLeft = effect.toInt() + } + if (mShadowSide and TOP == TOP) { + rectTop = effect + paddingTop = effect.toInt() + } + if (mShadowSide and RIGHT == RIGHT) { + rectRight = this.measuredWidth - effect + paddingRight = effect.toInt() + } + if (mShadowSide and BOTTOM == BOTTOM) { + rectBottom = this.measuredHeight - effect + paddingBottom = effect.toInt() + } + if (mShadowDy != 0.0f) { + rectBottom -= mShadowDy + paddingBottom += mShadowDy.toInt() + } + if (mShadowDx != 0.0f) { + rectRight -= mShadowDx + paddingRight += mShadowDx.toInt() + } + mRectF.left = rectLeft + mRectF.top = rectTop + mRectF.right = rectRight + mRectF.bottom = rectBottom + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom) + } + + /** + * 真正绘制阴影的方法 + */ + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + setUpShadowPaint() + if (mShadowShape == SHAPE_RECTANGLE) { + canvas.drawRect(mRectF, mPaint) + } else if (mShadowShape == SHAPE_OVAL) { + canvas.drawCircle( + mRectF.centerX(), + mRectF.centerY(), + mRectF.width().coerceAtMost(mRectF.height()) / 2, + mPaint + ) + } + } + + fun setShadowColor(shadowColor: Int) { + mShadowColor = shadowColor + requestLayout() + postInvalidate() + } + + fun setShadowRadius(shadowRadius: Float) { + mShadowRadius = shadowRadius + requestLayout() + postInvalidate() + } + + private fun setUpShadowPaint() { + mPaint.reset() + mPaint.isAntiAlias = true + mPaint.color = Color.TRANSPARENT + mPaint.setShadowLayer(mShadowRadius, mShadowDx, mShadowDy, mShadowColor) + } + + /** + * dip2px dp 值转 px 值 + * + * @param dpValue dp 值 + * @return px 值 + */ + private fun dip2px(dpValue: Float): Float { + val dm = context.resources.displayMetrics + val scale = dm.density + return dpValue * scale + 0.5f + } + + companion object { + const val ALL = 0x1111 + const val LEFT = 0x0001 + const val TOP = 0x0010 + const val RIGHT = 0x0100 + const val BOTTOM = 0x1000 + const val SHAPE_RECTANGLE = 0x0001 + const val SHAPE_OVAL = 0x0010 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt new file mode 100755 index 000000000..3a4f89f68 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -0,0 +1,274 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.Menu +import android.view.View +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.StyleRes +import androidx.appcompat.widget.Toolbar +import androidx.core.graphics.alpha +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.children +import com.google.android.material.appbar.AppBarLayout +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.elevation +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.activity +import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat +import splitties.views.bottomPadding +import splitties.views.topPadding + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class TitleBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppBarLayout(context, attrs) { + + val toolbar: Toolbar + val menu: Menu + get() = toolbar.menu + + var title: CharSequence? + get() = toolbar.title + set(title) { + if (toolbar.title != title) { + toolbar.title = title + } + } + + var subtitle: CharSequence? + get() = toolbar.subtitle + set(subtitle) { + if (toolbar.subtitle != subtitle) { + toolbar.subtitle = subtitle + } + } + + private val displayHomeAsUp: Boolean + private val navigationIconTint: ColorStateList? + private val navigationIconTintMode: Int + private val fitStatusBar: Boolean + private val fitNavigationBar: Boolean + private val attachToActivity: Boolean + + init { + val a = context.obtainStyledAttributes( + attrs, R.styleable.TitleBar, + R.attr.titleBarStyle, 0 + ) + navigationIconTint = a.getColorStateList(R.styleable.TitleBar_navigationIconTint) + navigationIconTintMode = a.getInt(R.styleable.TitleBar_navigationIconTintMode, 9) + attachToActivity = a.getBoolean(R.styleable.TitleBar_attachToActivity, true) + displayHomeAsUp = a.getBoolean(R.styleable.TitleBar_displayHomeAsUp, true) + fitStatusBar = a.getBoolean(R.styleable.TitleBar_fitStatusBar, true) + fitNavigationBar = a.getBoolean(R.styleable.TitleBar_fitNavigationBar, false) + + val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon) + val navigationContentDescription = + a.getText(R.styleable.TitleBar_navigationContentDescription) + val titleText = a.getString(R.styleable.TitleBar_title) + val subtitleText = a.getString(R.styleable.TitleBar_subtitle) + + when (a.getInt(R.styleable.TitleBar_themeMode, 0)) { + 1 -> inflate(context, R.layout.view_title_bar_dark, this) + else -> inflate(context, R.layout.view_title_bar, this) + } + toolbar = findViewById(R.id.toolbar) + + toolbar.apply { + navigationIcon?.let { + this.navigationIcon = it + this.navigationContentDescription = navigationContentDescription + } + + if (a.hasValue(R.styleable.TitleBar_titleTextAppearance)) { + this.setTitleTextAppearance( + context, + a.getResourceId(R.styleable.TitleBar_titleTextAppearance, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_titleTextColor)) { + this.setTitleTextColor(a.getColor(R.styleable.TitleBar_titleTextColor, -0x1)) + } + + if (a.hasValue(R.styleable.TitleBar_subtitleTextAppearance)) { + this.setSubtitleTextAppearance( + context, + a.getResourceId(R.styleable.TitleBar_subtitleTextAppearance, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_subtitleTextColor)) { + this.setSubtitleTextColor(a.getColor(R.styleable.TitleBar_subtitleTextColor, -0x1)) + } + + + if (a.hasValue(R.styleable.TitleBar_contentInsetLeft) + || a.hasValue(R.styleable.TitleBar_contentInsetRight) + ) { + this.setContentInsetsAbsolute( + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetLeft, 0), + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetRight, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_contentInsetStart) + || a.hasValue(R.styleable.TitleBar_contentInsetEnd) + ) { + this.setContentInsetsRelative( + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetStart, 0), + a.getDimensionPixelSize(R.styleable.TitleBar_contentInsetEnd, 0) + ) + } + + if (a.hasValue(R.styleable.TitleBar_contentInsetStartWithNavigation)) { + this.contentInsetStartWithNavigation = a.getDimensionPixelOffset( + R.styleable.TitleBar_contentInsetStartWithNavigation, 0 + ) + } + + if (a.hasValue(R.styleable.TitleBar_contentInsetEndWithActions)) { + this.contentInsetEndWithActions = a.getDimensionPixelOffset( + R.styleable.TitleBar_contentInsetEndWithActions, 0 + ) + } + + if (!titleText.isNullOrBlank()) { + this.title = titleText + } + + if (!subtitleText.isNullOrBlank()) { + this.subtitle = subtitleText + } + + if (a.hasValue(R.styleable.TitleBar_contentLayout)) { + inflate(context, a.getResourceId(R.styleable.TitleBar_contentLayout, 0), this) + } + } + + if (!isInEditMode) { +// if (fitStatusBar) { +// setPadding(paddingLeft, context.statusBarHeight, paddingRight, paddingBottom) +// } +// +// if (fitNavigationBar) { +// setPadding(paddingLeft, paddingTop, paddingRight, context.navigationBarHeight) +// } + + if (fitStatusBar || fitNavigationBar) { + setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + if (fitStatusBar) { + topPadding = insets.top + } + if (fitNavigationBar) { + bottomPadding = insets.bottom + } + windowInsets + } + } + + if (AppConfig.isEInkMode) { + setBackgroundResource(R.drawable.bg_eink_border_bottom) + } else { + setBackgroundColor(context.primaryColor) + } + + stateListAnimator = null + elevation = context.elevation + } + a.recycle() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + attachToActivity() + } + + fun setNavigationOnClickListener(clickListener: ((View) -> Unit)) { + toolbar.setNavigationOnClickListener(clickListener) + } + + fun setTitle(titleId: Int) { + toolbar.setTitle(titleId) + } + + fun setSubTitle(subtitleId: Int) { + toolbar.setSubtitle(subtitleId) + } + + fun setTitleTextColor(@ColorInt color: Int) { + toolbar.setTitleTextColor(color) + } + + fun setTitleTextAppearance(@StyleRes resId: Int) { + toolbar.setTitleTextAppearance(context, resId) + } + + fun setSubTitleTextColor(@ColorInt color: Int) { + toolbar.setSubtitleTextColor(color) + } + + fun setSubTitleTextAppearance(@StyleRes resId: Int) { + toolbar.setSubtitleTextAppearance(context, resId) + } + + fun setTextColor(@ColorInt color: Int) { + setTitleTextColor(color) + setSubTitleTextColor(color) + } + + fun setColorFilter(@ColorInt color: Int) { + val colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + toolbar.children.firstOrNull { it is ImageView }?.background?.colorFilter = colorFilter + toolbar.navigationIcon?.colorFilter = colorFilter + toolbar.overflowIcon?.colorFilter = colorFilter + toolbar.menu.children.forEach { + it.icon?.colorFilter = colorFilter + } + } + + override fun setBackgroundColor(color: Int) { + if (color.alpha < 255) { + //这里不能改为0f,改为0f在横屏模式下文字和图标颜色会变 + elevation = 0.1f + } + super.setBackgroundColor(color) + } + + override fun setBackground(background: Drawable?) { + if (background is ColorDrawable) { + if (background.alpha < 255) { + //这里不能改为0f,改为0f在横屏模式下文字和图标颜色会变 + elevation = 0.1f + } + } + super.setBackground(background) + } + + fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, fullScreen: Boolean) { +// if (fitStatusBar) { +// val topPadding = if (!isInMultiWindowMode && fullScreen) context.statusBarHeight else 0 +// setPadding(paddingLeft, topPadding, paddingRight, paddingBottom) +// } + } + + private fun attachToActivity() { + if (attachToActivity) { + activity?.let { + it.setSupportActionBar(toolbar) + it.supportActionBar?.setDisplayHomeAsUpEnabled(displayHomeAsUp) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt b/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt new file mode 100755 index 000000000..ad2a46b39 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/RefreshProgressBar.kt @@ -0,0 +1,195 @@ +package io.legado.app.ui.widget.anima + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.os.Looper +import android.util.AttributeSet +import android.view.View + +import io.legado.app.R + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class RefreshProgressBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + private var a = 1 + private var durProgress = 0 + private var secondDurProgress = 0 + var maxProgress = 100 + var secondMaxProgress = 100 + var bgColor = 0x00000000 + var secondColor = -0x3e3e3f + var fontColor = -0xc9c9ca + var speed = 2 + var secondFinalProgress = 0 + private set + private var paint: Paint = Paint() + private val bgRect = Rect() + private val secondRect = Rect() + private val fontRectF = RectF() + + var isAutoLoading: Boolean = false + set(loading) { + field = loading + if (!loading) { + secondDurProgress = 0 + secondFinalProgress = 0 + } + maxProgress = 0 + + invalidate() + } + + init { + paint.style = Paint.Style.FILL + + val a = context.obtainStyledAttributes(attrs, R.styleable.RefreshProgressBar) + speed = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_speed, speed) + maxProgress = a.getInt(R.styleable.RefreshProgressBar_max_progress, maxProgress) + durProgress = a.getInt(R.styleable.RefreshProgressBar_dur_progress, durProgress) + secondDurProgress = a.getDimensionPixelSize( + R.styleable.RefreshProgressBar_second_dur_progress, + secondDurProgress + ) + secondFinalProgress = secondDurProgress + secondMaxProgress = a.getDimensionPixelSize( + R.styleable.RefreshProgressBar_second_max_progress, + secondMaxProgress + ) + bgColor = a.getColor(R.styleable.RefreshProgressBar_bg_color, bgColor) + secondColor = a.getColor(R.styleable.RefreshProgressBar_second_color, secondColor) + fontColor = a.getColor(R.styleable.RefreshProgressBar_font_color, fontColor) + a.recycle() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + paint.color = bgColor + bgRect.set(0, 0, measuredWidth, measuredHeight) + canvas.drawRect(bgRect, paint) + + if (secondDurProgress > 0 && secondMaxProgress > 0) { + var secondDur = secondDurProgress + if (secondDur > secondMaxProgress) { + secondDur = secondMaxProgress + } + paint.color = secondColor + val tempW = + (measuredWidth.toFloat() * 1.0f * (secondDur * 1.0f / secondMaxProgress)).toInt() + secondRect.set( + measuredWidth / 2 - tempW / 2, + 0, + measuredWidth / 2 + tempW / 2, + measuredHeight + ) + canvas.drawRect(secondRect, paint) + } + + if (durProgress > 0 && maxProgress > 0) { + paint.color = fontColor + fontRectF.set( + 0f, + 0f, + measuredWidth.toFloat() * 1.0f * (durProgress * 1.0f / maxProgress), + measuredHeight.toFloat() + ) + canvas.drawRect(fontRectF, paint) + } + + if (this.isAutoLoading) { + if (secondDurProgress >= secondMaxProgress) { + a = -1 + } else if (secondDurProgress <= 0) { + a = 1 + } + secondDurProgress += a * speed + if (secondDurProgress < 0) + secondDurProgress = 0 + else if (secondDurProgress > secondMaxProgress) + secondDurProgress = secondMaxProgress + secondFinalProgress = secondDurProgress + invalidate() + } else { + if (secondDurProgress != secondFinalProgress) { + if (secondDurProgress > secondFinalProgress) { + secondDurProgress -= speed + if (secondDurProgress < secondFinalProgress) { + secondDurProgress = secondFinalProgress + } + } else { + secondDurProgress += speed + if (secondDurProgress > secondFinalProgress) { + secondDurProgress = secondFinalProgress + } + } + this.invalidate() + } + } + } + + fun getDurProgress(): Int { + return durProgress + } + + fun setDurProgress(durProgress: Int) { + var durProgress1 = durProgress + if (durProgress1 < 0) { + durProgress1 = 0 + } + if (durProgress1 > maxProgress) { + durProgress1 = maxProgress + } + this.durProgress = durProgress1 + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } + + fun getSecondDurProgress(): Int { + return secondDurProgress + } + + fun setSecondDurProgress(secondDur: Int) { + this.secondDurProgress = secondDur + this.secondFinalProgress = secondDurProgress + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } + + fun setSecondDurProgressWithAnim(secondDur: Int) { + var secondDur1 = secondDur + if (secondDur1 < 0) { + secondDur1 = 0 + } + if (secondDur1 > secondMaxProgress) { + secondDur1 = secondMaxProgress + } + this.secondFinalProgress = secondDur1 + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } + + fun clean() { + durProgress = 0 + secondDurProgress = 0 + secondFinalProgress = 0 + if (Looper.myLooper() == Looper.getMainLooper()) { + this.invalidate() + } else { + this.postInvalidate() + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt b/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt new file mode 100755 index 000000000..2138936e6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/RotateLoading.kt @@ -0,0 +1,228 @@ +package io.legado.app.ui.widget.anima + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.dpToPx + +/** + * RotateLoading + * Created by Victor on 2015/4/28. + */ +@Suppress("MemberVisibilityCanBePrivate") +class RotateLoading @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + + private var mPaint: Paint + + private var loadingRectF: RectF? = null + private var shadowRectF: RectF? = null + + private var topDegree = 10 + private var bottomDegree = 190 + + private var arc: Float = 0.toFloat() + + private var thisWidth: Int = 0 + + private var changeBigger = true + + private var shadowPosition: Int = 0 + + private var hideMode = GONE + + var isStarted = false + private set + + var loadingColor: Int = 0 + set(value) { + field = value + invalidate() + } + + private var speedOfDegree: Int = 0 + + private var speedOfArc: Float = 0.toFloat() + + private val shown = Runnable { this.startInternal() } + + private val hidden = Runnable { this.stopInternal() } + + init { + loadingColor = context.accentColor + thisWidth = DEFAULT_WIDTH.dpToPx() + shadowPosition = DEFAULT_SHADOW_POSITION.dpToPx() + speedOfDegree = DEFAULT_SPEED_OF_DEGREE + + if (null != attrs) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RotateLoading) + loadingColor = + typedArray.getColor(R.styleable.RotateLoading_loading_color, loadingColor) + thisWidth = typedArray.getDimensionPixelSize( + R.styleable.RotateLoading_loading_width, + DEFAULT_WIDTH.dpToPx() + ) + shadowPosition = typedArray.getInt( + R.styleable.RotateLoading_shadow_position, + DEFAULT_SHADOW_POSITION + ) + speedOfDegree = + typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE) + typedArray.recycle() + } + speedOfArc = (speedOfDegree / 4).toFloat() + mPaint = Paint() + mPaint.color = loadingColor + mPaint.isAntiAlias = true + mPaint.style = Paint.Style.STROKE + mPaint.strokeWidth = thisWidth.toFloat() + mPaint.strokeCap = Paint.Cap.ROUND + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + arc = 10f + + loadingRectF = + RectF( + (2 * thisWidth).toFloat(), + (2 * thisWidth).toFloat(), + (w - 2 * thisWidth).toFloat(), + (h - 2 * thisWidth).toFloat() + ) + shadowRectF = RectF( + (2 * thisWidth + shadowPosition).toFloat(), + (2 * thisWidth + shadowPosition).toFloat(), + (w - 2 * thisWidth + shadowPosition).toFloat(), + (h - 2 * thisWidth + shadowPosition).toFloat() + ) + } + + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (!isStarted) { + return + } + + mPaint.color = Color.parseColor("#1a000000") + shadowRectF?.let { + canvas.drawArc(it, topDegree.toFloat(), arc, false, mPaint) + canvas.drawArc(it, bottomDegree.toFloat(), arc, false, mPaint) + } + + mPaint.color = loadingColor + loadingRectF?.let { + canvas.drawArc(it, topDegree.toFloat(), arc, false, mPaint) + canvas.drawArc(it, bottomDegree.toFloat(), arc, false, mPaint) + } + + topDegree += speedOfDegree + bottomDegree += speedOfDegree + if (topDegree > 360) { + topDegree -= 360 + } + if (bottomDegree > 360) { + bottomDegree -= 360 + } + + if (changeBigger) { + if (arc < 160) { + arc += speedOfArc + invalidate() + } + } else { + if (arc > speedOfDegree) { + arc -= 2 * speedOfArc + invalidate() + } + } + if (arc >= 160 || arc <= 10) { + changeBigger = !changeBigger + invalidate() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + if (visibility == VISIBLE) { + startInternal() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + isStarted = false + animate().cancel() + removeCallbacks(shown) + removeCallbacks(hidden) + } + + fun visible() { + removeCallbacks(shown) + removeCallbacks(hidden) + post(shown) + } + + fun inVisible() { + hideMode = INVISIBLE + removeCallbacks(shown) + removeCallbacks(hidden) + stopInternal() + } + + fun gone() { + hideMode = GONE + removeCallbacks(shown) + removeCallbacks(hidden) + stopInternal() + } + + private fun startInternal() { + startAnimator() + isStarted = true + invalidate() + } + + private fun stopInternal() { + stopAnimator() + invalidate() + } + + private fun startAnimator() { + animate().cancel() + animate().scaleX(1.0f) + .scaleY(1.0f) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + visibility = VISIBLE + } + }) + .start() + } + + private fun stopAnimator() { + animate().cancel() + isStarted = false + this.visibility = hideMode + } + + companion object { + private const val DEFAULT_WIDTH = 6 + private const val DEFAULT_SHADOW_POSITION = 2 + private const val DEFAULT_SPEED_OF_DEGREE = 10 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt new file mode 100755 index 000000000..582f7239c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.graphics.* +import android.view.View +import android.view.animation.AccelerateInterpolator +import java.util.* +import kotlin.math.pow + +@SuppressLint("Recycle") +class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rect) : + ValueAnimator() { + + private val mPaint: Paint = Paint() + private val mParticles: Array + private val mBound: Rect = Rect(bound) + + init { + val partLen = 15 + mParticles = arrayOfNulls(partLen * partLen) + val random = Random(System.currentTimeMillis()) + val w = bitmap.width / (partLen + 2) + val h = bitmap.height / (partLen + 2) + for (i in 0 until partLen) { + for (j in 0 until partLen) { + mParticles[i * partLen + j] = + generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random) + } + } + setFloatValues(0f, END_VALUE) + interpolator = DEFAULT_INTERPOLATOR + duration = DEFAULT_DURATION + } + + private fun generateParticle(color: Int, random: Random): Particle { + val particle = Particle() + particle.color = color + particle.radius = V + if (random.nextFloat() < 0.2f) { + particle.baseRadius = V + (X - V) * random.nextFloat() + } else { + particle.baseRadius = W + (V - W) * random.nextFloat() + } + val nextFloat = random.nextFloat() + particle.top = mBound.height() * (0.18f * random.nextFloat() + 0.2f) + particle.top = + if (nextFloat < 0.2f) particle.top else particle.top + particle.top * 0.2f * random.nextFloat() + particle.bottom = mBound.height() * (random.nextFloat() - 0.5f) * 1.8f + var f = + if (nextFloat < 0.2f) particle.bottom else if (nextFloat < 0.8f) particle.bottom * 0.6f else particle.bottom * 0.3f + particle.bottom = f + particle.mag = 4.0f * particle.top / particle.bottom + particle.neg = -particle.mag / particle.bottom + f = mBound.centerX() + Y * (random.nextFloat() - 0.5f) + particle.baseCx = f + particle.cx = f + f = mBound.centerY() + Y * (random.nextFloat() - 0.5f) + particle.baseCy = f + particle.cy = f + particle.life = END_VALUE / 10 * random.nextFloat() + particle.overflow = 0.4f * random.nextFloat() + particle.alpha = 1f + return particle + } + + fun draw(canvas: Canvas): Boolean { + if (!isStarted) { + return false + } + for (particle in mParticles) { + particle?.let { + particle.advance(animatedValue as Float) + if (particle.alpha > 0f) { + mPaint.color = particle.color + mPaint.alpha = (Color.alpha(particle.color) * particle.alpha).toInt() + canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint) + } + } + } + mContainer.invalidate() + return true + } + + override fun start() { + super.start() + mContainer.invalidate() + } + + private inner class Particle { + var alpha: Float = 0.toFloat() + var color: Int = 0 + var cx: Float = 0.toFloat() + var cy: Float = 0.toFloat() + var radius: Float = 0.toFloat() + var baseCx: Float = 0.toFloat() + var baseCy: Float = 0.toFloat() + var baseRadius: Float = 0.toFloat() + var top: Float = 0.toFloat() + var bottom: Float = 0.toFloat() + var mag: Float = 0.toFloat() + var neg: Float = 0.toFloat() + var life: Float = 0.toFloat() + var overflow: Float = 0.toFloat() + + + fun advance(factor: Float) { + var f = 0f + var normalization = factor / END_VALUE + if (normalization < life || normalization > 1f - overflow) { + alpha = 0f + return + } + normalization = (normalization - life) / (1f - life - overflow) + val f2 = normalization * END_VALUE + if (normalization >= 0.7f) { + f = (normalization - 0.7f) / 0.3f + } + alpha = 1f - f + f = bottom * f2 + cx = baseCx + f + cy = (baseCy - this.neg * f.toDouble().pow(2.0)).toFloat() - f * mag + radius = V + (baseRadius - V) * f2 + } + } + + companion object { + + internal var DEFAULT_DURATION: Long = 0x400 + private val DEFAULT_INTERPOLATOR = AccelerateInterpolator(0.6f) + private const val END_VALUE = 1.4f + private val X = Utils.dp2Px(5).toFloat() + private val Y = Utils.dp2Px(20).toFloat() + private val V = Utils.dp2Px(2).toFloat() + private val W = Utils.dp2Px(1).toFloat() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt new file mode 100755 index 000000000..9b014b7d2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt @@ -0,0 +1,21 @@ +package io.legado.app.ui.widget.anima.explosion_field + +import android.app.Activity +import android.view.View +import android.view.ViewGroup +import android.view.Window + +object ExplosionField { + + fun attach2Window(activity: Activity): ExplosionView { + val rootView = activity.findViewById(Window.ID_ANDROID_CONTENT) as ViewGroup + val explosionField = ExplosionView(activity) + rootView.addView( + explosionField, ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + ) + return explosionField + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt new file mode 100755 index 000000000..39bd13780 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.media.MediaPlayer +import android.util.AttributeSet +import android.view.View +import io.legado.app.utils.DebugLog + +import java.util.* + + +@Suppress("unused") +class ExplosionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + View(context, attrs) { + + private var customDuration = ExplosionAnimator.DEFAULT_DURATION + private var idPlayAnimationEffect = 0 + private var mZAnimatorListener: OnAnimatorListener? = null + private var mOnClickListener: OnClickListener? = null + + private val mExplosions = ArrayList() + private val mExpandInset = IntArray(2) + + init { + Arrays.fill(mExpandInset, Utils.dp2Px(32)) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + for (explosion in mExplosions) { + explosion.draw(canvas) + } + } + + fun playSoundAnimationEffect(id: Int) { + this.idPlayAnimationEffect = id + } + + fun setCustomDuration(customDuration: Long) { + this.customDuration = customDuration + } + + fun addActionEvent(iEvents: OnAnimatorListener) { + this.mZAnimatorListener = iEvents + } + + + fun expandExplosionBound(dx: Int, dy: Int) { + mExpandInset[0] = dx + mExpandInset[1] = dy + } + + @JvmOverloads + fun explode(bitmap: Bitmap?, bound: Rect, startDelay: Long, view: View? = null) { + val currentDuration = customDuration + val explosion = ExplosionAnimator(this, bitmap!!, bound) + explosion.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mExplosions.remove(animation) + view?.let { + view.scaleX = 1f + view.scaleY = 1f + view.alpha = 1f + //view.setOnClickListener(mOnClickListener)//set event + } + } + }) + explosion.startDelay = startDelay + explosion.duration = currentDuration + mExplosions.add(explosion) + explosion.start() + } + + @JvmOverloads + fun explode(view: View, restartState: Boolean? = false) { + + val r = Rect() + view.getGlobalVisibleRect(r) + val location = IntArray(2) + getLocationOnScreen(location) + r.offset(-location[0], -location[1]) + r.inset(-mExpandInset[0], -mExpandInset[1]) + val startDelay = 100 + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150) + animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { + + var random = Random() + + override fun onAnimationUpdate(animation: ValueAnimator) { + view.translationX = (random.nextFloat() - 0.5f) * view.width.toFloat() * 0.05f + view.translationY = (random.nextFloat() - 0.5f) * view.height.toFloat() * 0.05f + } + }) + + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) { + if (idPlayAnimationEffect != 0) + MediaPlayer.create(context, idPlayAnimationEffect).start() + } + + override fun onAnimationEnd(animator: Animator) { + if (mZAnimatorListener != null) { + mZAnimatorListener!!.onAnimationEnd(animator, this@ExplosionView) + } + } + + override fun onAnimationCancel(animator: Animator) { + DebugLog.i(javaClass.name, "CANCEL") + } + + override fun onAnimationRepeat(animator: Animator) { + DebugLog.i(javaClass.name, "REPEAT") + } + }) + + animator.start() + view.animate().setDuration(150).setStartDelay(startDelay.toLong()).scaleX(0f).scaleY(0f) + .alpha(0f).start() + if (restartState!!) + explode(Utils.createBitmapFromView(view), r, startDelay.toLong(), view) + else + explode(Utils.createBitmapFromView(view), r, startDelay.toLong()) + + } + + fun clear() { + mExplosions.clear() + invalidate() + } + + override fun setOnClickListener(mOnClickListener: OnClickListener?) { + this.mOnClickListener = mOnClickListener + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt new file mode 100755 index 000000000..13a04c670 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/OnAnimatorListener.kt @@ -0,0 +1,8 @@ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.Animator +import android.view.View + +interface OnAnimatorListener { + fun onAnimationEnd(animator: Animator, view: View) +} diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt new file mode 100755 index 000000000..0c2446e05 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/Utils.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.view.View +import android.widget.ImageView +import io.legado.app.utils.printOnDebug + + +import kotlin.math.roundToInt + +object Utils { + + private val DENSITY = Resources.getSystem().displayMetrics.density + private val sCanvas = Canvas() + + fun dp2Px(dp: Int): Int { + return (dp * DENSITY).roundToInt() + } + + fun createBitmapFromView(view: View): Bitmap? { + if (view is ImageView) { + val drawable = view.drawable + if (drawable != null && drawable is BitmapDrawable) { + return drawable.bitmap + } + } + view.clearFocus() + val bitmap = createBitmapSafely( + view.width, + view.height, Bitmap.Config.ARGB_8888, 1 + ) + if (bitmap != null) { + synchronized(sCanvas) { + val canvas = sCanvas + canvas.setBitmap(bitmap) + view.draw(canvas) + canvas.setBitmap(null) + } + } + return bitmap + } + + private fun createBitmapSafely( + width: Int, + height: Int, + config: Bitmap.Config, + retryCount: Int + ): Bitmap? { + try { + return Bitmap.createBitmap(width, height, config) + } catch (e: OutOfMemoryError) { + e.printOnDebug() + if (retryCount > 0) { + System.gc() + return createBitmapSafely(width, height, config, retryCount - 1) + } + return null + } + + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt b/app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt new file mode 100755 index 000000000..7afa0e27a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt @@ -0,0 +1,327 @@ +package io.legado.app.ui.widget.checkbox + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import android.view.animation.LinearInterpolator +import android.widget.Checkable +import androidx.core.view.postDelayed +import io.legado.app.R +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatColor +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sqrt + +class SmoothCheckBox @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs), Checkable { + private var mPaint: Paint + private var mTickPaint: Paint + private var mFloorPaint: Paint + private var mTickPoints: Array + private var mCenterPoint: Point + private var mTickPath: Path + private var mLeftLineDistance = 0f + private var mRightLineDistance = 0f + private var mDrewDistance = 0f + private var mScaleVal = 1.0f + private var mFloorScale = 1.0f + private var mWidth = 0 + private var mAnimDuration = 0 + private var mStrokeWidth = 0 + private var mCheckedColor = 0 + private var mUnCheckedColor = 0 + private var mFloorColor = 0 + private var mFloorUnCheckedColor = 0 + private var mChecked = false + private var mTickDrawing = false + var onCheckedChangeListener: ((checkBox: SmoothCheckBox, isChecked: Boolean) -> Unit)? = null + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox) + var tickColor = ThemeStore.accentColor(context) + mCheckedColor = context.getCompatColor(R.color.background_menu) + mUnCheckedColor = context.getCompatColor(R.color.background_menu) + mFloorColor = context.getCompatColor(R.color.transparent30) + tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, tickColor) + mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION) + mFloorColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, mFloorColor) + mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, mCheckedColor) + mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, mUnCheckedColor) + mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, 0) + ta.recycle() + mFloorUnCheckedColor = mFloorColor + mTickPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mTickPaint.style = Paint.Style.STROKE + mTickPaint.strokeCap = Paint.Cap.ROUND + mTickPaint.color = tickColor + mFloorPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mFloorPaint.style = Paint.Style.FILL + mFloorPaint.color = mFloorColor + mPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mPaint.style = Paint.Style.FILL + mPaint.color = mCheckedColor + mTickPath = Path() + mCenterPoint = Point() + mTickPoints = arrayOf(Point(), Point(), Point()) + setOnClickListener { + toggle() + mTickDrawing = false + mDrewDistance = 0f + if (isChecked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + } + } + + override fun isChecked(): Boolean { + return mChecked + } + + override fun setChecked(checked: Boolean) { + mChecked = checked + reset() + invalidate() + onCheckedChangeListener?.invoke(this@SmoothCheckBox, mChecked) + } + + override fun toggle() { + this.isChecked = !isChecked + } + + /** + * checked with animation + * + * @param checked checked + * @param animate change with animation + */ + fun setChecked(checked: Boolean, animate: Boolean) { + if (animate) { + mTickDrawing = false + mChecked = checked + mDrewDistance = 0f + if (checked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + onCheckedChangeListener?.invoke(this@SmoothCheckBox, mChecked) + } else { + this.isChecked = checked + } + } + + private fun reset() { + mTickDrawing = true + mFloorScale = 1.0f + mScaleVal = if (isChecked) 0f else 1.0f + mFloorColor = if (isChecked) mCheckedColor else mFloorUnCheckedColor + mDrewDistance = if (isChecked) mLeftLineDistance + mRightLineDistance else 0f + } + + private fun measureSize(measureSpec: Int): Int { + val defSize: Int = DEF_DRAW_SIZE.dpToPx() + val specSize = MeasureSpec.getSize(measureSpec) + val specMode = MeasureSpec.getMode(measureSpec) + var result = 0 + when (specMode) { + MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> result = min(defSize, specSize) + MeasureSpec.EXACTLY -> result = specSize + } + return result + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)) + } + + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + mWidth = measuredWidth + mStrokeWidth = if (mStrokeWidth == 0) measuredWidth / 10 else mStrokeWidth + mStrokeWidth = + if (mStrokeWidth > measuredWidth / 5) measuredWidth / 5 else mStrokeWidth + mStrokeWidth = if (mStrokeWidth < 3) 3 else mStrokeWidth + mCenterPoint.x = mWidth / 2 + mCenterPoint.y = measuredHeight / 2 + mTickPoints[0].x = (measuredWidth.toFloat() / 30 * 7).roundToInt() + mTickPoints[0].y = (measuredHeight.toFloat() / 30 * 14).roundToInt() + mTickPoints[1].x = (measuredWidth.toFloat() / 30 * 13).roundToInt() + mTickPoints[1].y = (measuredHeight.toFloat() / 30 * 20).roundToInt() + mTickPoints[2].x = (measuredWidth.toFloat() / 30 * 22).roundToInt() + mTickPoints[2].y = (measuredHeight.toFloat() / 30 * 10).roundToInt() + mLeftLineDistance = sqrt( + (mTickPoints[1].x - mTickPoints[0].x.toDouble()).pow(2.0) + + (mTickPoints[1].y - mTickPoints[0].y.toDouble()).pow(2.0) + ).toFloat() + mRightLineDistance = sqrt( + (mTickPoints[2].x - mTickPoints[1].x.toDouble()).pow(2.0) + + (mTickPoints[2].y - mTickPoints[1].y.toDouble()).pow(2.0) + ).toFloat() + mTickPaint.strokeWidth = mStrokeWidth.toFloat() + } + + override fun onDraw(canvas: Canvas) { + drawBorder(canvas) + drawCenter(canvas) + drawTick(canvas) + } + + private fun drawCenter(canvas: Canvas) { + mPaint.color = mUnCheckedColor + val radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal + canvas.drawCircle(mCenterPoint.x.toFloat(), mCenterPoint.y.toFloat(), radius, mPaint) + } + + private fun drawBorder(canvas: Canvas) { + mFloorPaint.color = mFloorColor + val radius = mCenterPoint.x + canvas.drawCircle( + mCenterPoint.x.toFloat(), + mCenterPoint.y.toFloat(), + radius * mFloorScale, + mFloorPaint + ) + } + + private fun drawTick(canvas: Canvas) { + if (mTickDrawing && isChecked) { + drawTickPath(canvas) + } + } + + private fun drawTickPath(canvas: Canvas) { + mTickPath.reset() + // draw left of the tick + if (mDrewDistance < mLeftLineDistance) { + val step: Float = if (mWidth / 20.0f < 3) 3f else mWidth / 20.0f + mDrewDistance += step + val stopX = + mTickPoints[0].x + (mTickPoints[1].x - mTickPoints[0].x) * mDrewDistance / mLeftLineDistance + val stopY = + mTickPoints[0].y + (mTickPoints[1].y - mTickPoints[0].y) * mDrewDistance / mLeftLineDistance + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(stopX, stopY) + canvas.drawPath(mTickPath, mTickPaint) + if (mDrewDistance > mLeftLineDistance) { + mDrewDistance = mLeftLineDistance + } + } else { + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + // draw right of the tick + if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { + val stopX = + mTickPoints[1].x + (mTickPoints[2].x - mTickPoints[1].x) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance + val stopY = + mTickPoints[1].y - (mTickPoints[1].y - mTickPoints[2].y) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance + mTickPath.reset() + mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(stopX, stopY) + canvas.drawPath(mTickPath, mTickPaint) + val step: Float = if (mWidth / 20f < 3) 3f else mWidth / 20f + mDrewDistance += step + } else { + mTickPath.reset() + mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(mTickPoints[2].x.toFloat(), mTickPoints[2].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + } + } + // invalidate + if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { + postDelayed(10) { this.postInvalidate() } + } + } + + private fun startCheckedAnimation() { + val animator = ValueAnimator.ofFloat(1.0f, 0f) + animator.duration = mAnimDuration / 3 * 2.toLong() + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { animation: ValueAnimator -> + mScaleVal = animation.animatedValue as Float + mFloorColor = getGradientColor( + mUnCheckedColor, + mCheckedColor, + 1 - mScaleVal + ) + postInvalidate() + } + animator.start() + val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) + floorAnimator.duration = mAnimDuration.toLong() + floorAnimator.interpolator = LinearInterpolator() + floorAnimator.addUpdateListener { animation: ValueAnimator -> + mFloorScale = animation.animatedValue as Float + postInvalidate() + } + floorAnimator.start() + drawTickDelayed() + } + + private fun startUnCheckedAnimation() { + val animator = ValueAnimator.ofFloat(0f, 1.0f) + animator.duration = mAnimDuration.toLong() + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { animation: ValueAnimator -> + mScaleVal = animation.animatedValue as Float + mFloorColor = getGradientColor( + mCheckedColor, + mFloorUnCheckedColor, + mScaleVal + ) + postInvalidate() + } + animator.start() + val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) + floorAnimator.duration = mAnimDuration.toLong() + floorAnimator.interpolator = LinearInterpolator() + floorAnimator.addUpdateListener { animation: ValueAnimator -> + mFloorScale = animation.animatedValue as Float + postInvalidate() + } + floorAnimator.start() + } + + private fun drawTickDelayed() { + postDelayed(mAnimDuration.toLong()) { + mTickDrawing = true + postInvalidate() + } + } + + companion object { + private const val DEF_DRAW_SIZE = 25 + private const val DEF_ANIM_DURATION = 300 + private fun getGradientColor(startColor: Int, endColor: Int, percent: Float): Int { + val startA = Color.alpha(startColor) + val startR = Color.red(startColor) + val startG = Color.green(startColor) + val startB = Color.blue(startColor) + val endA = Color.alpha(endColor) + val endR = Color.red(endColor) + val endG = Color.green(endColor) + val endB = Color.blue(endColor) + val currentA = (startA * (1 - percent) + endA * percent).toInt() + val currentR = (startR * (1 - percent) + endR * percent).toInt() + val currentG = (startG * (1 - percent) + endG * percent).toInt() + val currentB = (startB * (1 - percent) + endB * percent).toInt() + return Color.argb(currentA, currentR, currentG, currentB) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/code/CodeView.kt b/app/src/main/java/io/legado/app/ui/widget/code/CodeView.kt new file mode 100755 index 000000000..30f578436 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/code/CodeView.kt @@ -0,0 +1,412 @@ +package io.legado.app.ui.widget.code + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Paint.FontMetricsInt +import android.graphics.Rect +import android.os.Handler +import android.os.Looper +import android.text.* +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.text.style.ReplacementSpan +import android.util.AttributeSet +import androidx.annotation.ColorInt +import io.legado.app.ui.widget.text.ScrollMultiAutoCompleteTextView +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern +import kotlin.math.roundToInt + +@Suppress("unused") +class CodeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ScrollMultiAutoCompleteTextView(context, attrs) { + + private var tabWidth = 0 + private var tabWidthInCharacters = 0 + private var mUpdateDelayTime = 500 + private var modified = true + private var highlightWhileTextChanging = true + private var hasErrors = false + private var mRemoveErrorsWhenTextChanged = true + private val mUpdateHandler = Handler(Looper.getMainLooper()) + private var mAutoCompleteTokenizer: Tokenizer? = null + private val displayDensity = resources.displayMetrics.density + private val mErrorHashSet: SortedMap = TreeMap() + private val mSyntaxPatternMap: MutableMap = HashMap() + private var mIndentCharacterList = mutableListOf('{', '+', '-', '*', '/', '=') + + private val mUpdateRunnable = Runnable { + val source = text + highlightWithoutChange(source) + } + + private val mEditorTextWatcher: TextWatcher = object : TextWatcher { + private var start = 0 + private var count = 0 + override fun beforeTextChanged( + charSequence: CharSequence, + start: Int, + before: Int, + count: Int + ) { + this.start = start + this.count = count + } + + override fun onTextChanged( + charSequence: CharSequence, + start: Int, + before: Int, + count: Int + ) { + if (!modified) return + if (highlightWhileTextChanging) { + if (mSyntaxPatternMap.isNotEmpty()) { + convertTabs(editableText, start, count) + mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime.toLong()) + } + } + if (mRemoveErrorsWhenTextChanged) removeAllErrorLines() + } + + override fun afterTextChanged(editable: Editable) { + if (!highlightWhileTextChanging) { + if (!modified) return + cancelHighlighterRender() + if (mSyntaxPatternMap.isNotEmpty()) { + convertTabs(editableText, start, count) + mUpdateHandler.postDelayed(mUpdateRunnable, mUpdateDelayTime.toLong()) + } + } + } + } + + init { + if (mAutoCompleteTokenizer == null) { + mAutoCompleteTokenizer = KeywordTokenizer() + } + setTokenizer(mAutoCompleteTokenizer) + filters = arrayOf( + InputFilter { source, start, end, dest, dStart, dEnd -> + if (modified && end - start == 1 && start < source.length && dStart < dest.length) { + val c = source[start] + if (c == '\n') { + return@InputFilter autoIndent(source, dest, dStart, dEnd) + } + } + source + } + ) + addTextChangedListener(mEditorTextWatcher) + } + + override fun showDropDown() { + val screenPoint = IntArray(2) + getLocationOnScreen(screenPoint) + val displayFrame = Rect() + getWindowVisibleDisplayFrame(displayFrame) + val position = selectionStart + val layout = layout + val line = layout.getLineForOffset(position) + val verticalDistanceInDp = (750 + 140 * line) / displayDensity + dropDownVerticalOffset = verticalDistanceInDp.toInt() + val horizontalDistanceInDp = layout.getPrimaryHorizontal(position) / displayDensity + dropDownHorizontalOffset = horizontalDistanceInDp.toInt() + super.showDropDown() + } + + private fun autoIndent( + source: CharSequence, + dest: Spanned, + dStart: Int, + dEnd: Int + ): CharSequence { + var indent = "" + var iStart = dStart - 1 + var dataBefore = false + var pt = 0 + while (iStart > -1) { + val c = dest[iStart] + if (c == '\n') break + if (c != ' ' && c != '\t') { + if (!dataBefore) { + if (mIndentCharacterList.contains(c)) --pt + dataBefore = true + } + if (c == '(') { + --pt + } else if (c == ')') { + ++pt + } + } + --iStart + } + if (iStart > -1) { + val charAtCursor = dest[dStart] + var iEnd: Int = ++iStart + while (iEnd < dEnd) { + val c = dest[iEnd] + if (charAtCursor != '\n' && c == '/' && iEnd + 1 < dEnd && dest[iEnd] == c) { + iEnd += 2 + break + } + if (c != ' ' && c != '\t') { + break + } + ++iEnd + } + indent += dest.subSequence(iStart, iEnd) + } + if (pt < 0) { + indent += "\t" + } + return source.toString() + indent + } + + private fun highlightSyntax(editable: Editable) { + if (mSyntaxPatternMap.isEmpty()) return + for (pattern in mSyntaxPatternMap.keys) { + val color = mSyntaxPatternMap[pattern]!! + val m = pattern.matcher(editable) + while (m.find()) { + createForegroundColorSpan(editable, m, color) + } + } + } + + private fun highlightErrorLines(editable: Editable) { + if (mErrorHashSet.isEmpty()) return + val maxErrorLineValue = mErrorHashSet.lastKey() + var lineNumber = 0 + val matcher = PATTERN_LINE.matcher(editable) + while (matcher.find()) { + if (mErrorHashSet.containsKey(lineNumber)) { + val color = mErrorHashSet[lineNumber]!! + createBackgroundColorSpan(editable, matcher, color) + } + lineNumber += 1 + if (lineNumber > maxErrorLineValue) break + } + } + + private fun createForegroundColorSpan( + editable: Editable, + matcher: Matcher, + @ColorInt color: Int + ) { + editable.setSpan( + ForegroundColorSpan(color), + matcher.start(), matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + private fun createBackgroundColorSpan( + editable: Editable, + matcher: Matcher, + @ColorInt color: Int + ) { + editable.setSpan( + BackgroundColorSpan(color), + matcher.start(), matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + private fun highlight(editable: Editable): Editable { + // if (editable.isEmpty() || editable.length > 1024) return editable + if (editable.length !in 1..1024) { + return editable + } + try { + clearSpans(editable) + highlightErrorLines(editable) + highlightSyntax(editable) + } catch (e: IllegalStateException) { + e.printStackTrace() + } + return editable + } + + private fun highlightWithoutChange(editable: Editable) { + modified = false + highlight(editable) + modified = true + } + + fun setTextHighlighted(text: CharSequence?) { + if (text.isNullOrEmpty()) return + cancelHighlighterRender() + removeAllErrorLines() + modified = false + setText(highlight(SpannableStringBuilder(text))) + modified = true + } + + fun setTabWidth(characters: Int) { + if (tabWidthInCharacters == characters) return + tabWidthInCharacters = characters + tabWidth = (paint.measureText("m") * characters).roundToInt() + } + + private fun clearSpans(editable: Editable) { + val length = editable.length + val foregroundSpans = editable.getSpans( + 0, length, ForegroundColorSpan::class.java + ) + run { + var i = foregroundSpans.size + while (i-- > 0) { + editable.removeSpan(foregroundSpans[i]) + } + } + val backgroundSpans = editable.getSpans( + 0, length, BackgroundColorSpan::class.java + ) + var i = backgroundSpans.size + while (i-- > 0) { + editable.removeSpan(backgroundSpans[i]) + } + } + + fun cancelHighlighterRender() { + mUpdateHandler.removeCallbacks(mUpdateRunnable) + } + + private fun convertTabs(editable: Editable, start: Int, count: Int) { + var startIndex = start + if (tabWidth < 1) return + val s = editable.toString() + val stop = startIndex + count + while (s.indexOf("\t", startIndex).also { startIndex = it } > -1 && startIndex < stop) { + editable.setSpan( + TabWidthSpan(), + startIndex, + startIndex + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + ++startIndex + } + } + + fun setSyntaxPatternsMap(syntaxPatterns: Map?) { + if (mSyntaxPatternMap.isNotEmpty()) mSyntaxPatternMap.clear() + mSyntaxPatternMap.putAll(syntaxPatterns!!) + } + + fun addSyntaxPattern(pattern: Pattern, @ColorInt Color: Int) { + mSyntaxPatternMap[pattern] = Color + } + + fun removeSyntaxPattern(pattern: Pattern) { + mSyntaxPatternMap.remove(pattern) + } + + fun getSyntaxPatternsSize(): Int { + return mSyntaxPatternMap.size + } + + fun resetSyntaxPatternList() { + mSyntaxPatternMap.clear() + } + + fun setAutoIndentCharacterList(characterList: MutableList) { + mIndentCharacterList = characterList + } + + fun clearAutoIndentCharacterList() { + mIndentCharacterList.clear() + } + + fun getAutoIndentCharacterList(): List { + return mIndentCharacterList + } + + fun addErrorLine(lineNum: Int, color: Int) { + mErrorHashSet[lineNum] = color + hasErrors = true + } + + fun removeErrorLine(lineNum: Int) { + mErrorHashSet.remove(lineNum) + hasErrors = mErrorHashSet.size > 0 + } + + fun removeAllErrorLines() { + mErrorHashSet.clear() + hasErrors = false + } + + fun getErrorsSize(): Int { + return mErrorHashSet.size + } + + fun getTextWithoutTrailingSpace(): String { + return PATTERN_TRAILING_WHITE_SPACE + .matcher(text) + .replaceAll("") + } + + fun setAutoCompleteTokenizer(tokenizer: Tokenizer?) { + mAutoCompleteTokenizer = tokenizer + } + + fun setRemoveErrorsWhenTextChanged(removeErrors: Boolean) { + mRemoveErrorsWhenTextChanged = removeErrors + } + + fun reHighlightSyntax() { + highlightSyntax(editableText) + } + + fun reHighlightErrors() { + highlightErrorLines(editableText) + } + + fun isHasError(): Boolean { + return hasErrors + } + + fun setUpdateDelayTime(time: Int) { + mUpdateDelayTime = time + } + + fun getUpdateDelayTime(): Int { + return mUpdateDelayTime + } + + fun setHighlightWhileTextChanging(updateWhileTextChanging: Boolean) { + highlightWhileTextChanging = updateWhileTextChanging + } + + private inner class TabWidthSpan : ReplacementSpan() { + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: FontMetricsInt? + ): Int { + return tabWidth + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + } + } + + companion object { + private val PATTERN_LINE = Pattern.compile("(^.+$)+", Pattern.MULTILINE) + private val PATTERN_TRAILING_WHITE_SPACE = Pattern.compile("[\\t ]+$", Pattern.MULTILINE) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/code/CodeViewExtensions.kt b/app/src/main/java/io/legado/app/ui/widget/code/CodeViewExtensions.kt new file mode 100755 index 000000000..afefa8cd9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/code/CodeViewExtensions.kt @@ -0,0 +1,35 @@ +@file:Suppress("unused") + +package io.legado.app.ui.widget.code + +import android.content.Context +import android.widget.ArrayAdapter +import io.legado.app.R +import splitties.init.appCtx +import splitties.resources.color +import java.util.regex.Pattern + +val legadoPattern: Pattern = Pattern.compile("\\|\\||&&|%%|@js:|@Json:|@css:|@@|@XPath:") +val jsonPattern: Pattern = Pattern.compile("\"[A-Za-z0-9]*?\"\\:|\"|\\{|\\}|\\[|\\]") +val wrapPattern: Pattern = Pattern.compile("\\\\n") +val operationPattern: Pattern = + Pattern.compile(":|==|>|<|!=|>=|<=|->|=|%|-|-=|%=|\\+|\\-|\\-=|\\+=|\\^|\\&|\\|::|\\?|\\*") +val jsPattern: Pattern = Pattern.compile("var") + +fun CodeView.addLegadoPattern() { + addSyntaxPattern(legadoPattern, appCtx.color(R.color.md_orange_900)) +} + +fun CodeView.addJsonPattern() { + addSyntaxPattern(jsonPattern, appCtx.color(R.color.md_blue_800)) +} + +fun CodeView.addJsPattern() { + addSyntaxPattern(wrapPattern, appCtx.color(R.color.md_blue_grey_500)) + addSyntaxPattern(operationPattern, appCtx.color(R.color.md_orange_900)) + addSyntaxPattern(jsPattern, appCtx.color(R.color.md_light_blue_600)) +} + +fun Context.arrayAdapter(keywords: Array): ArrayAdapter { + return ArrayAdapter(this, R.layout.item_1line_text_and_del, R.id.text_view, keywords) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.kt b/app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.kt new file mode 100755 index 000000000..607fc9b9c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/code/KeywordTokenizer.kt @@ -0,0 +1,25 @@ +package io.legado.app.ui.widget.code + +import android.widget.MultiAutoCompleteTextView +import kotlin.math.max + +class KeywordTokenizer : MultiAutoCompleteTextView.Tokenizer { + override fun findTokenStart(charSequence: CharSequence, cursor: Int): Int { + var sequenceStr = charSequence.toString() + sequenceStr = sequenceStr.substring(0, cursor) + val spaceIndex = sequenceStr.lastIndexOf(" ") + val lineIndex = sequenceStr.lastIndexOf("\n") + val bracketIndex = sequenceStr.lastIndexOf("(") + val index = max(0, max(spaceIndex, max(lineIndex, bracketIndex))) + if (index == 0) return 0 + return if (index + 1 < charSequence.length) index + 1 else index + } + + override fun findTokenEnd(charSequence: CharSequence, cursor: Int): Int { + return charSequence.length + } + + override fun terminateToken(charSequence: CharSequence): CharSequence { + return charSequence + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/CodeDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/CodeDialog.kt new file mode 100755 index 000000000..d2063c1f0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/CodeDialog.kt @@ -0,0 +1,77 @@ +package io.legado.app.ui.widget.dialog + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogCodeViewBinding +import io.legado.app.help.IntentData +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.code.addJsPattern +import io.legado.app.ui.widget.code.addJsonPattern +import io.legado.app.ui.widget.code.addLegadoPattern +import io.legado.app.utils.applyTint +import io.legado.app.utils.disableEdit +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class CodeDialog() : BaseDialogFragment(R.layout.dialog_code_view) { + + constructor(code: String, disableEdit: Boolean = true, requestId: String? = null) : this() { + arguments = Bundle().apply { + putBoolean("disableEdit", disableEdit) + putString("code", IntentData.put(code)) + putString("requestId", requestId) + } + } + + val binding by viewBinding(DialogCodeViewBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + if (arguments?.getBoolean("disableEdit") == true) { + binding.toolBar.title = "code view" + binding.codeView.disableEdit() + } else { + initMenu() + } + binding.codeView.addLegadoPattern() + binding.codeView.addJsonPattern() + binding.codeView.addJsPattern() + arguments?.getString("code")?.let { + binding.codeView.text = IntentData.get(it) + } + } + + private fun initMenu() { + binding.toolBar.inflateMenu(R.menu.code_edit) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_save -> { + binding.codeView.text?.toString()?.let { code -> + val requestId = arguments?.getString("requestId") + (parentFragment as? Callback)?.onCodeSave(code, requestId) + ?: (activity as? Callback)?.onCodeSave(code, requestId) + } + dismiss() + } + } + return@setOnMenuItemClickListener true + } + } + + + interface Callback { + + fun onCodeSave(code: String, requestId: String?) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt new file mode 100755 index 000000000..5154dc84b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt @@ -0,0 +1,71 @@ +package io.legado.app.ui.widget.dialog + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.request.RequestOptions +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogPhotoViewBinding +import io.legado.app.help.book.BookHelp +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.model.BookCover +import io.legado.app.model.ImageProvider +import io.legado.app.model.ReadBook +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +/** + * 显示图片 + */ +class PhotoDialog() : BaseDialogFragment(R.layout.dialog_photo_view) { + + constructor(src: String, sourceOrigin: String? = null) : this() { + arguments = Bundle().apply { + putString("src", src) + putString("sourceOrigin", sourceOrigin) + } + } + + private val binding by viewBinding(DialogPhotoViewBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + } + + @SuppressLint("CheckResult") + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val arguments = arguments ?: return + val src = arguments.getString("src") ?: return + ImageProvider.get(src)?.let { + binding.photoView.setImageBitmap(it) + return + } + val file = ReadBook.book?.let { book -> + BookHelp.getImage(book, src) + } + if (file?.exists() == true) { + ImageLoader.load(requireContext(), file) + .error(R.drawable.image_loading_error) + .dontTransform() + .downsample(DownsampleStrategy.NONE) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(binding.photoView) + } else { + ImageLoader.load(requireContext(), src).apply { + arguments.getString("sourceOrigin")?.let { sourceOrigin -> + apply(RequestOptions().set(OkHttpModelLoader.sourceOriginOption, sourceOrigin)) + } + }.error(BookCover.defaultDrawable) + .dontTransform() + .downsample(DownsampleStrategy.NONE) + .into(binding.photoView) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt new file mode 100755 index 000000000..8bd006634 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt @@ -0,0 +1,125 @@ +package io.legado.app.ui.widget.dialog + +import android.os.Build +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.view.textclassifier.TextClassifier +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.databinding.DialogTextViewBinding +import io.legado.app.help.IntentData +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.applyTint +import io.legado.app.utils.setHtml +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.noties.markwon.Markwon +import io.noties.markwon.ext.tables.TablePlugin +import io.noties.markwon.html.HtmlPlugin +import io.noties.markwon.image.glide.GlideImagesPlugin +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class TextDialog() : BaseDialogFragment(R.layout.dialog_text_view) { + + enum class Mode { + MD, HTML, TEXT + } + + constructor( + title: String, + content: String?, + mode: Mode = Mode.TEXT, + time: Long = 0, + autoClose: Boolean = false + ) : this() { + arguments = Bundle().apply { + putString("title", title) + putString("content", IntentData.put(content)) + putString("mode", mode.name) + putLong("time", time) + } + isCancelable = false + this.autoClose = autoClose + } + + private val binding by viewBinding(DialogTextViewBinding::bind) + private var time = 0L + private var autoClose: Boolean = false + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.inflateMenu(R.menu.dialog_text) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_close -> dismissAllowingStateLoss() + } + true + } + arguments?.let { + binding.toolBar.title = it.getString("title") + val content = IntentData.get(it.getString("content")) ?: "" + when (it.getString("mode")) { + Mode.MD.name -> viewLifecycleOwner.lifecycleScope.launch { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + binding.textView.setTextClassifier(TextClassifier.NO_OP) + } + val markwon: Markwon + val markdown = withContext(IO) { + markwon = Markwon.builder(requireContext()) + .usePlugin(GlideImagesPlugin.create(requireContext())) + .usePlugin(HtmlPlugin.create()) + .usePlugin(TablePlugin.create(requireContext())) + .build() + markwon.toMarkdown(content) + } + markwon.setParsedMarkdown(binding.textView, markdown) + } + + Mode.HTML.name -> binding.textView.setHtml(content) + else -> { + if (content.length >= 32 * 1024) { + val truncatedContent = + content.substring(0, 32 * 1024) + "\n\n数据太大,无法全部显示…" + binding.textView.text = truncatedContent + } else { + binding.textView.text = content + } + } + } + time = it.getLong("time", 0L) + } + if (time > 0) { + binding.badgeView.setBadgeCount((time / 1000).toInt()) + lifecycleScope.launch { + while (time > 0) { + delay(1000) + time -= 1000 + binding.badgeView.setBadgeCount((time / 1000).toInt()) + if (time <= 0) { + view.post { + dialog?.setCancelable(true) + if (autoClose) dialog?.cancel() + } + } + } + } + } else { + view.post { + dialog?.setCancelable(true) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt new file mode 100755 index 000000000..0c4825f0a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextListDialog.kt @@ -0,0 +1,81 @@ +package io.legado.app.ui.widget.dialog + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemLogBinding +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +@Suppress("unused") +class TextListDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) { + + constructor(title: String, values: ArrayList) : this() { + arguments = Bundle().apply { + putString("title", title) + putStringArrayList("values", values) + } + } + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { TextAdapter(requireContext()) } + private var values: ArrayList? = null + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run { + arguments?.let { + toolBar.title = it.getString("title") + values = it.getStringArrayList("values") + } + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter + adapter.setItems(values) + } + + class TextAdapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemLogBinding { + return ItemLogBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemLogBinding, + item: String, + payloads: MutableList + ) { + binding.apply { + if (textView.getTag(R.id.tag1) == null) { + val listener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + textView.isCursorVisible = false + textView.isCursorVisible = true + } + + override fun onViewDetachedFromWindow(v: View) {} + } + textView.addOnAttachStateChangeListener(listener) + textView.setTag(R.id.tag1, listener) + } + textView.text = item + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) { + //nothing + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/UrlOptionDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/UrlOptionDialog.kt new file mode 100755 index 000000000..2b4a02bb2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/UrlOptionDialog.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.widget.dialog + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.databinding.DialogUrlOptionEditBinding +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.GSON +import io.legado.app.utils.setLayout + +class UrlOptionDialog(context: Context, private val success: (String) -> Unit) : Dialog(context) { + + val binding = DialogUrlOptionEditBinding.inflate(layoutInflater) + + override fun onStart() { + super.onStart() + setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT) + window?.setBackgroundDrawableResource(R.color.transparent) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + binding.root.setOnClickListener { dismiss() } + binding.vwBg.setOnClickListener(null) + binding.editMethod.setFilterValues("POST", "GET") + binding.editCharset.setFilterValues(AppConst.charsets) + binding.tvOk.setOnClickListener { + success.invoke(GSON.toJson(getUrlOption())) + dismiss() + } + } + + private fun getUrlOption(): AnalyzeUrl.UrlOption { + val urlOption = AnalyzeUrl.UrlOption() + urlOption.useWebView(binding.cbUseWebView.isChecked) + urlOption.setMethod(binding.editMethod.text.toString()) + urlOption.setCharset(binding.editCharset.text.toString()) + urlOption.setHeaders(binding.editHeaders.text.toString()) + urlOption.setBody(binding.editBody.text.toString()) + urlOption.setRetry(binding.editRetry.text.toString()) + urlOption.setType(binding.editType.text.toString()) + urlOption.setWebJs(binding.editWebJs.text.toString()) + urlOption.setJs(binding.editJs.text.toString()) + return urlOption + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/VariableDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/VariableDialog.kt new file mode 100755 index 000000000..6e00cf842 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/VariableDialog.kt @@ -0,0 +1,96 @@ +package io.legado.app.ui.widget.dialog + +import android.app.Application +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.BaseViewModel +import io.legado.app.databinding.DialogVariableBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.applyTint +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + +class VariableDialog() : BaseDialogFragment(R.layout.dialog_variable, true), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogVariableBinding::bind) + private val viewModel by viewModels() + + constructor(title: String, key: String, variable: String?, comment: String) : this() { + arguments = Bundle().apply { + putString("title", title) + putString("key", key) + putString("variable", variable) + putString("comment", comment) + } + } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + arguments?.let { + binding.toolBar.title = it.getString("title") + viewModel.init(it) { + binding.tvComment.text = viewModel.comment + binding.tvVariable.setText(viewModel.variable) + } + } ?: let { + dismiss() + return + } + binding.toolBar.inflateMenu(R.menu.save) + binding.toolBar.menu.applyTint(requireContext()) + binding.toolBar.setOnMenuItemClickListener(this) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_save -> { + callback?.setVariable( + viewModel.key ?: "", + binding.tvVariable.text?.toString() + ) + dismissAllowingStateLoss() + } + } + return true + } + + val callback get() = (parentFragment as? Callback) ?: (activity as? Callback) + + class ViewModel(application: Application) : BaseViewModel(application) { + + var key: String? = null + var comment: String? = null + var variable: String? = null + + fun init(arguments: Bundle, onFinally: () -> Unit) { + if (key != null) return + execute { + key = arguments.getString("key") + comment = arguments.getString("comment") + variable = arguments.getString("variable") + }.onFinally { + onFinally.invoke() + } + } + + } + + interface Callback { + + fun setVariable(key: String, variable: String?) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt new file mode 100755 index 000000000..9e2d9ed2f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/WaitDialog.kt @@ -0,0 +1,28 @@ +package io.legado.app.ui.widget.dialog + +import android.app.Dialog +import android.content.Context +import io.legado.app.databinding.DialogWaitBinding + + +@Suppress("unused") +class WaitDialog(context: Context) : Dialog(context) { + + val binding = DialogWaitBinding.inflate(layoutInflater) + + init { + setCanceledOnTouchOutside(false) + setContentView(binding.root) + } + + fun setText(text: String): WaitDialog { + binding.tvMsg.text = text + return this + } + + fun setText(res: Int): WaitDialog { + binding.tvMsg.setText(res) + return this + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt b/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt new file mode 100755 index 000000000..c2a45147f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/DynamicFrameLayout.kt @@ -0,0 +1,192 @@ +package io.legado.app.ui.widget.dynamiclayout + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.View +import android.view.ViewStub +import android.widget.FrameLayout +import android.widget.ProgressBar +import androidx.appcompat.widget.AppCompatButton +import androidx.appcompat.widget.AppCompatImageView +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R + +@Suppress("unused") +class DynamicFrameLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs), ViewSwitcher { + + private var errorView: View? = null + private var errorImage: AppCompatImageView? = null + private var errorTextView: AppCompatTextView? = null + private var actionBtn: AppCompatButton? = null + + private var progressView: View? = null + private var progressBar: ProgressBar? = null + + private var contentView: View? = null + + private var errorIcon: Drawable? = null + private var emptyIcon: Drawable? = null + + private var errorActionDescription: CharSequence? = null + private var emptyActionDescription: CharSequence? = null + private var emptyDescription: CharSequence? = null + + private var errorAction: Action? = null + private var emptyAction: Action? = null + + private var changeListener: OnVisibilityChangeListener? = null + + init { + View.inflate(context, R.layout.view_dynamic, this) + + val a = context.obtainStyledAttributes(attrs, R.styleable.DynamicFrameLayout) + errorIcon = a.getDrawable(R.styleable.DynamicFrameLayout_errorSrc) + emptyIcon = a.getDrawable(R.styleable.DynamicFrameLayout_emptySrc) + + emptyActionDescription = a.getText(R.styleable.DynamicFrameLayout_emptyActionDescription) + emptyDescription = a.getText(R.styleable.DynamicFrameLayout_emptyDescription) + + errorActionDescription = a.getText(R.styleable.DynamicFrameLayout_errorActionDescription) + if (errorActionDescription == null) { + errorActionDescription = context.getString(R.string.dynamic_click_retry) + } + a.recycle() + } + + override fun onFinishInflate() { + super.onFinishInflate() + if (childCount > 2) { + contentView = getChildAt(2) + } + } + + override fun showErrorView(message: CharSequence) { + ensureErrorView() + + setViewVisible(errorView, true) + setViewVisible(contentView, false) + setViewVisible(progressView, false) + + errorTextView?.text = message + errorImage?.setImageDrawable(errorIcon) + + actionBtn?.let { + it.tag = ACTION_WHEN_ERROR + it.visibility = View.VISIBLE + if (errorActionDescription != null) { + it.text = errorActionDescription + } + } + + dispatchVisibilityChanged(ViewSwitcher.SHOW_ERROR_VIEW) + } + + override fun showErrorView(messageId: Int) { + showErrorView(resources.getText(messageId)) + } + + override fun showEmptyView() { + ensureErrorView() + + setViewVisible(errorView, true) + setViewVisible(contentView, false) + setViewVisible(progressView, false) + + errorTextView?.text = emptyDescription + errorImage?.setImageDrawable(emptyIcon) + + actionBtn?.let { + it.tag = ACTION_WHEN_EMPTY + if (errorActionDescription != null) { + it.visibility = View.VISIBLE + it.text = errorActionDescription + } else { + it.visibility = View.INVISIBLE + } + } + + dispatchVisibilityChanged(ViewSwitcher.SHOW_EMPTY_VIEW) + } + + override fun showProgressView() { + ensureProgressView() + + setViewVisible(errorView, false) + setViewVisible(contentView, false) + setViewVisible(progressView, true) + + dispatchVisibilityChanged(ViewSwitcher.SHOW_PROGRESS_VIEW) + } + + override fun showContentView() { + setViewVisible(errorView, false) + setViewVisible(contentView, true) + setViewVisible(progressView, false) + + dispatchVisibilityChanged(ViewSwitcher.SHOW_CONTENT_VIEW) + } + + fun setOnVisibilityChangeListener(listener: OnVisibilityChangeListener) { + changeListener = listener + } + + fun setErrorAction(action: Action) { + errorAction = action + } + + fun setEmptyAction(action: Action) { + emptyAction = action + } + + private fun setViewVisible(view: View?, visible: Boolean) { + view?.let { + it.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + } + + private fun ensureErrorView() { + if (errorView == null) { + errorView = findViewById(R.id.error_view_stub).inflate() + errorImage = errorView?.findViewById(R.id.iv_error_image) + errorTextView = errorView?.findViewById(R.id.tv_error_message) + actionBtn = errorView?.findViewById(R.id.btn_error_retry) + + actionBtn?.setOnClickListener { + when (it.tag) { + ACTION_WHEN_EMPTY -> emptyAction?.onAction(this@DynamicFrameLayout) + ACTION_WHEN_ERROR -> errorAction?.onAction(this@DynamicFrameLayout) + } + } + } + } + + private fun ensureProgressView() { + if (progressView == null) { + progressView = findViewById(R.id.progress_view_stub).inflate() + progressBar = progressView?.findViewById(R.id.loading_progress) + } + } + + private fun dispatchVisibilityChanged(@ViewSwitcher.Visibility visibility: Int) { + changeListener?.onVisibilityChanged(visibility) + } + + interface Action { + fun onAction(switcher: ViewSwitcher) + } + + + interface OnVisibilityChangeListener { + + fun onVisibilityChanged(@ViewSwitcher.Visibility visibility: Int) + } + + companion object { + private const val ACTION_WHEN_ERROR = "ACTION_WHEN_ERROR" + private const val ACTION_WHEN_EMPTY = "ACTION_WHEN_EMPTY" + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/ViewSwitcher.kt b/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/ViewSwitcher.kt new file mode 100755 index 000000000..dbcc0f132 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dynamiclayout/ViewSwitcher.kt @@ -0,0 +1,29 @@ +package io.legado.app.ui.widget.dynamiclayout + +import androidx.annotation.IntDef +import androidx.annotation.StringRes + +interface ViewSwitcher { + + companion object { + const val SHOW_CONTENT_VIEW = 0 + const val SHOW_ERROR_VIEW = 1 + const val SHOW_EMPTY_VIEW = 2 + const val SHOW_PROGRESS_VIEW = 3 + } + + @Retention(AnnotationRetention.SOURCE) + @IntDef(SHOW_CONTENT_VIEW, SHOW_ERROR_VIEW, SHOW_EMPTY_VIEW, SHOW_PROGRESS_VIEW) + annotation class Visibility + + fun showErrorView(message: CharSequence) + + fun showErrorView(@StringRes messageId: Int) + + fun showEmptyView() + + fun showProgressView() + + fun showContentView() + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/ArcView.kt b/app/src/main/java/io/legado/app/ui/widget/image/ArcView.kt new file mode 100755 index 000000000..b2bfc241a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/ArcView.kt @@ -0,0 +1,87 @@ +package io.legado.app.ui.widget.image + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import io.legado.app.R + +/** + * 弧形View + */ +class ArcView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + private var mWidth = 0 + private var mHeight = 0 + + //弧形高度 + private val mArcHeight: Int + + //背景颜色 + private var mBgColor: Int + private val mPaint: Paint = Paint().apply { + isAntiAlias = true + } + private val mDirectionTop: Boolean + val rect = Rect() + val path = Path() + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ArcView) + mArcHeight = typedArray.getDimensionPixelSize(R.styleable.ArcView_arcHeight, 0) + mBgColor = typedArray.getColor( + R.styleable.ArcView_bgColor, + Color.parseColor("#303F9F") + ) + mDirectionTop = typedArray.getBoolean(R.styleable.ArcView_arcDirectionTop, false) + typedArray.recycle() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + mPaint.style = Paint.Style.FILL + mPaint.color = mBgColor + if (mDirectionTop) { + rect.set(0, mArcHeight, mWidth, mHeight) + canvas.drawRect(rect, mPaint) + path.reset() + path.moveTo(0f, mArcHeight.toFloat()) + path.quadTo(mWidth / 2.toFloat(), 0f, mWidth.toFloat(), mArcHeight.toFloat()) + canvas.drawPath(path, mPaint) + } else { + rect.set(0, 0, mWidth, mHeight - mArcHeight) + canvas.drawRect(rect, mPaint) + path.reset() + path.moveTo(0f, mHeight - mArcHeight.toFloat()) + path.quadTo( + mWidth / 2.toFloat(), + mHeight.toFloat(), + mWidth.toFloat(), + mHeight - mArcHeight.toFloat() + ) + canvas.drawPath(path, mPaint) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + if (widthMode == MeasureSpec.EXACTLY) { + mWidth = widthSize + } + if (heightMode == MeasureSpec.EXACTLY) { + mHeight = heightSize + } + setMeasuredDimension(mWidth, mHeight) + } + + fun setBgColor(color: Int) { + mBgColor = color + invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt new file mode 100755 index 000000000..192a4a761 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt @@ -0,0 +1,450 @@ +package io.legado.app.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.text.TextPaint +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.widget.AppCompatImageView +import io.legado.app.R +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.spToPx +import kotlin.math.min +import kotlin.math.pow + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class CircleImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + + private val mDrawableRect = RectF() + private val mBorderRect = RectF() + + private val mShaderMatrix = Matrix() + private val mBitmapPaint = Paint() + private val mBorderPaint = Paint() + private val mCircleBackgroundPaint = Paint() + private val textPaint by lazy { + val textPaint = TextPaint() + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint + } + + private var mBorderColor = DEFAULT_BORDER_COLOR + private var mBorderWidth = DEFAULT_BORDER_WIDTH + private var mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR + + private var mBitmap: Bitmap? = null + private var mBitmapShader: BitmapShader? = null + private var mBitmapWidth: Int = 0 + private var mBitmapHeight: Int = 0 + + private var mDrawableRadius: Float = 0.toFloat() + private var mBorderRadius: Float = 0.toFloat() + + private var mColorFilter: ColorFilter? = null + + private var mReady: Boolean = false + private var mSetupPending: Boolean = false + private var mBorderOverlay: Boolean = false + var isDisableCircularTransformation: Boolean = false + set(disableCircularTransformation) { + if (field == disableCircularTransformation) { + return + } + field = disableCircularTransformation + initializeBitmap() + } + + var borderColor: Int + get() = mBorderColor + set(@ColorInt borderColor) { + if (borderColor == mBorderColor) { + return + } + + mBorderColor = borderColor + mBorderPaint.color = mBorderColor + invalidate() + } + + var circleBackgroundColor: Int + get() = mCircleBackgroundColor + set(@ColorInt circleBackgroundColor) { + if (circleBackgroundColor == mCircleBackgroundColor) { + return + } + mCircleBackgroundColor = circleBackgroundColor + mCircleBackgroundPaint.color = circleBackgroundColor + invalidate() + } + + var borderWidth: Int + get() = mBorderWidth + set(borderWidth) { + if (borderWidth == mBorderWidth) { + return + } + + mBorderWidth = borderWidth + setup() + } + + var isBorderOverlay: Boolean + get() = mBorderOverlay + set(borderOverlay) { + if (borderOverlay == mBorderOverlay) { + return + } + + mBorderOverlay = borderOverlay + setup() + } + + private var text: String? = null + + private var textColor = context.getCompatColor(R.color.primaryText) + private var textBold = false + var isInView = false + + init { + val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView) + mBorderWidth = + a.getDimensionPixelSize( + R.styleable.CircleImageView_civ_border_width, + DEFAULT_BORDER_WIDTH + ) + mBorderColor = + a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR) + mBorderOverlay = + a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY) + mCircleBackgroundColor = + a.getColor( + R.styleable.CircleImageView_civ_circle_background_color, + DEFAULT_CIRCLE_BACKGROUND_COLOR + ) + text = a.getString(R.styleable.CircleImageView_text) + contentDescription = text + if (a.hasValue(R.styleable.CircleImageView_textColor)) { + textColor = a.getColor( + R.styleable.CircleImageView_textColor, + context.getCompatColor(R.color.primaryText) + ) + } + a.recycle() + + mReady = true + + if (mSetupPending) { + setup() + mSetupPending = false + } + } + + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + if (adjustViewBounds) { + throw IllegalArgumentException("adjustViewBounds not supported.") + } + } + + override fun onDraw(canvas: Canvas) { + if (isDisableCircularTransformation) { + super.onDraw(canvas) + return + } + if (mBitmap == null) { + return + } + + if (mCircleBackgroundColor != Color.TRANSPARENT) { + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mCircleBackgroundPaint + ) + } + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mBitmapPaint + ) + if (mBorderWidth > 0) { + canvas.drawCircle( + mBorderRect.centerX(), + mBorderRect.centerY(), + mBorderRadius, + mBorderPaint + ) + } + drawText(canvas) + } + + private fun drawText(canvas: Canvas) { + text?.let { + textPaint.color = textColor + textPaint.isFakeBoldText = textBold + textPaint.textSize = 15f.spToPx() + val fm = textPaint.fontMetrics + canvas.drawText( + it, + width * 0.5f, + (height * 0.5f + (fm.bottom - fm.top) * 0.5f - fm.bottom), + textPaint + ) + } + } + + fun setText(text: String?) { + this.text = text + contentDescription = text + invalidate() + } + + fun setTextColor(@ColorInt textColor: Int) { + this.textColor = textColor + invalidate() + } + + fun setTextBold(bold: Boolean) { + this.textBold = bold + invalidate() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + setup() + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + super.setPadding(left, top, right, bottom) + setup() + } + + override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { + super.setPaddingRelative(start, top, end, bottom) + setup() + } + + fun setCircleBackgroundColorResource(@ColorRes circleBackgroundRes: Int) { + circleBackgroundColor = context.getCompatColor(circleBackgroundRes) + } + + override fun setImageBitmap(bm: Bitmap) { + super.setImageBitmap(bm) + initializeBitmap() + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + initializeBitmap() + } + + override fun setImageResource(@DrawableRes resId: Int) { + super.setImageResource(resId) + initializeBitmap() + } + + override fun setImageURI(uri: Uri?) { + super.setImageURI(uri) + initializeBitmap() + } + + override fun setColorFilter(cf: ColorFilter) { + if (cf === mColorFilter) { + return + } + + mColorFilter = cf + applyColorFilter() + invalidate() + } + + override fun getColorFilter(): ColorFilter? { + return mColorFilter + } + + private fun applyColorFilter() { + mBitmapPaint.colorFilter = mColorFilter + } + + private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? { + if (drawable == null) { + return null + } + + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + return try { + val bitmap: Bitmap = if (drawable is ColorDrawable) { + Bitmap.createBitmap( + COLOR_DRAWABLE_DIMENSION, + COLOR_DRAWABLE_DIMENSION, + BITMAP_CONFIG + ) + } else { + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + BITMAP_CONFIG + ) + } + + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + bitmap + } catch (e: Exception) { + e.printOnDebug() + null + } + + } + + private fun initializeBitmap() { + mBitmap = if (isDisableCircularTransformation) { + null + } else { + getBitmapFromDrawable(drawable) + } + setup() + } + + private fun setup() { + if (!mReady) { + mSetupPending = true + return + } + + if (width == 0 && height == 0) { + return + } + + if (mBitmap == null) { + invalidate() + return + } + + mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + + mBitmapPaint.isAntiAlias = true + mBitmapPaint.shader = mBitmapShader + + mBorderPaint.style = Paint.Style.STROKE + mBorderPaint.isAntiAlias = true + mBorderPaint.color = mBorderColor + mBorderPaint.strokeWidth = mBorderWidth.toFloat() + + mCircleBackgroundPaint.style = Paint.Style.FILL + mCircleBackgroundPaint.isAntiAlias = true + mCircleBackgroundPaint.color = mCircleBackgroundColor + + mBitmapHeight = mBitmap!!.height + mBitmapWidth = mBitmap!!.width + + mBorderRect.set(calculateBounds()) + mBorderRadius = + min( + (mBorderRect.height() - mBorderWidth) / 2.0f, + (mBorderRect.width() - mBorderWidth) / 2.0f + ) + + mDrawableRect.set(mBorderRect) + if (!mBorderOverlay && mBorderWidth > 0) { + mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f) + } + mDrawableRadius = min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f) + + applyColorFilter() + updateShaderMatrix() + invalidate() + } + + private fun calculateBounds(): RectF { + val availableWidth = width - paddingLeft - paddingRight + val availableHeight = height - paddingTop - paddingBottom + + val sideLength = min(availableWidth, availableHeight) + + val left = paddingLeft + (availableWidth - sideLength) / 2f + val top = paddingTop + (availableHeight - sideLength) / 2f + + return RectF(left, top, left + sideLength, top + sideLength) + } + + private fun updateShaderMatrix() { + val scale: Float + var dx = 0f + var dy = 0f + + mShaderMatrix.set(null) + + if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { + scale = mDrawableRect.height() / mBitmapHeight.toFloat() + dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f + } else { + scale = mDrawableRect.width() / mBitmapWidth.toFloat() + dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f + } + + mShaderMatrix.setScale(scale, scale) + mShaderMatrix.postTranslate( + (dx + 0.5f).toInt() + mDrawableRect.left, + (dy + 0.5f).toInt() + mDrawableRect.top + ) + + mBitmapShader!!.setLocalMatrix(mShaderMatrix) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isInView = (inTouchableArea(event.x, event.y)) + } + } + return super.onTouchEvent(event) + } + + private fun inTouchableArea(x: Float, y: Float): Boolean { + return (x - mBorderRect.centerX()).toDouble() + .pow(2.0) + (y - mBorderRect.centerY()).toDouble() + .pow(2.0) <= mBorderRadius.toDouble().pow(2.0) + } + + private inner class OutlineProvider : ViewOutlineProvider() { + + override fun getOutline(view: View, outline: Outline) { + val bounds = Rect() + mBorderRect.roundOut(bounds) + outline.setRoundRect(bounds, bounds.width() / 2.0f) + } + + } + + companion object { + private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 + private const val COLOR_DRAWABLE_DIMENSION = 2 + private const val DEFAULT_BORDER_WIDTH = 0 + private const val DEFAULT_BORDER_COLOR = Color.BLACK + private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT + private const val DEFAULT_BORDER_OVERLAY = false + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt new file mode 100755 index 000000000..f67ab469b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt @@ -0,0 +1,254 @@ +package io.legado.app.ui.widget.image + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.TextPaint +import android.util.AttributeSet +import android.view.ViewGroup +import androidx.appcompat.widget.AppCompatImageView +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import io.legado.app.constant.AppPattern +import io.legado.app.help.config.AppConfig +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.lib.theme.accentColor +import io.legado.app.model.BookCover +import io.legado.app.utils.textHeight +import io.legado.app.utils.toStringArray + +/** + * 封面 + */ +@Suppress("unused") +class CoverImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + private var filletPath = Path() + private var viewWidth: Float = 0f + private var viewHeight: Float = 0f + private var defaultCover = true + var bitmapPath: String? = null + private set + private var name: String? = null + private var author: String? = null + private var nameHeight = 0f + private var authorHeight = 0f + private val namePaint by lazy { + val textPaint = TextPaint() + textPaint.typeface = Typeface.DEFAULT_BOLD + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint + } + private val authorPaint by lazy { + val textPaint = TextPaint() + textPaint.typeface = Typeface.DEFAULT + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint + } + + override fun setLayoutParams(params: ViewGroup.LayoutParams?) { + if (params != null) { + val width = params.width + if (width >= 0) { + params.height = width * 7 / 5 + } else { + params.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + } + super.setLayoutParams(params) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + val measuredHeight = measuredWidth * 7 / 5 + super.onMeasure( + widthMeasureSpec, + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + viewWidth = width.toFloat() + viewHeight = height.toFloat() + filletPath.reset() + if (width > 10 && viewHeight > 10) { + filletPath.apply { + moveTo(10f, 0f) + lineTo(viewWidth - 10, 0f) + quadTo(viewWidth, 0f, viewWidth, 10f) + lineTo(viewWidth, viewHeight - 10) + quadTo(viewWidth, viewHeight, viewWidth - 10, viewHeight) + lineTo(10f, viewHeight) + quadTo(0f, viewHeight, 0f, viewHeight - 10) + lineTo(0f, 10f) + quadTo(0f, 0f, 10f, 0f) + close() + } + } + } + + override fun onDraw(canvas: Canvas) { + if (!filletPath.isEmpty) { + canvas.clipPath(filletPath) + } + super.onDraw(canvas) + if (defaultCover && !isInEditMode) { + drawNameAuthor(canvas) + } + } + + private fun drawNameAuthor(canvas: Canvas) { + if (!BookCover.drawBookName) return + var startX = width * 0.2f + var startY = viewHeight * 0.2f + name?.toStringArray()?.let { name -> + namePaint.textSize = viewWidth / 6 + namePaint.strokeWidth = namePaint.textSize / 5 + name.forEachIndexed { index, char -> + namePaint.color = Color.WHITE + namePaint.style = Paint.Style.STROKE + canvas.drawText(char, startX, startY, namePaint) + namePaint.color = context.accentColor + namePaint.style = Paint.Style.FILL + canvas.drawText(char, startX, startY, namePaint) + startY += namePaint.textHeight + if (startY > viewHeight * 0.8) { + startX += namePaint.textSize + namePaint.textSize = viewWidth / 10 + startY = (viewHeight - (name.size - index - 1) * namePaint.textHeight) / 2 + } + } + } + if (!BookCover.drawBookAuthor) return + author?.toStringArray()?.let { author -> + authorPaint.textSize = viewWidth / 10 + authorPaint.strokeWidth = authorPaint.textSize / 5 + startX = width * 0.8f + startY = viewHeight * 0.95f - author.size * authorPaint.textHeight + startY = maxOf(startY, viewHeight * 0.3f) + author.forEach { + authorPaint.color = Color.WHITE + authorPaint.style = Paint.Style.STROKE + canvas.drawText(it, startX, startY, authorPaint) + authorPaint.color = context.accentColor + authorPaint.style = Paint.Style.FILL + canvas.drawText(it, startX, startY, authorPaint) + startY += authorPaint.textHeight + if (startY > viewHeight * 0.95) { + return@let + } + } + } + } + + fun setHeight(height: Int) { + val width = height * 5 / 7 + minimumWidth = width + } + + private val glideListener by lazy { + object : RequestListener { + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + defaultCover = true + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + defaultCover = false + return false + } + + } + } + + fun load( + path: String? = null, + name: String? = null, + author: String? = null, + loadOnlyWifi: Boolean = false, + sourceOrigin: String? = null, + fragment: Fragment? = null, + lifecycle: Lifecycle? = null, + onLoadFinish: (() -> Unit)? = null + ) { + this.bitmapPath = path + this.name = name?.replace(AppPattern.bdRegex, "")?.trim() + this.author = author?.replace(AppPattern.bdRegex, "")?.trim() + defaultCover = true + invalidate() + if (AppConfig.useDefaultCover) { + ImageLoader.load(context, BookCover.defaultDrawable) + .centerCrop() + .into(this) + } else { + var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi) + if (sourceOrigin != null) { + options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin) + } + var builder = if (fragment != null && lifecycle != null) { + ImageLoader.load(fragment, lifecycle, path) + } else { + ImageLoader.load(context, path)//Glide自动识别http://,content://和file:// + } + builder = builder.apply(options) + .placeholder(BookCover.defaultDrawable) + .error(BookCover.defaultDrawable) + .listener(glideListener) + if (onLoadFinish != null) { + builder = builder.addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + onLoadFinish.invoke() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + onLoadFinish.invoke() + return false + } + }) + } + builder + .centerCrop() + .into(this) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt new file mode 100755 index 000000000..f8f0b1807 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt @@ -0,0 +1,99 @@ +package io.legado.app.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Path +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import io.legado.app.R +import io.legado.app.utils.dpToPx +import kotlin.math.max + +class FilletImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + internal var width: Float = 0.toFloat() + internal var height: Float = 0.toFloat() + private var leftTopRadius: Int = 0 + private var rightTopRadius: Int = 0 + private var rightBottomRadius: Int = 0 + private var leftBottomRadius: Int = 0 + + init { + // 读取配置 + val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView) + val defaultRadius = 5.dpToPx() + val radius = + array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius) + leftTopRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_left_top_radius, + defaultRadius + ) + rightTopRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_right_top_radius, + defaultRadius + ) + rightBottomRadius = + array.getDimensionPixelOffset( + R.styleable.FilletImageView_right_bottom_radius, + defaultRadius + ) + leftBottomRadius = array.getDimensionPixelOffset( + R.styleable.FilletImageView_left_bottom_radius, + defaultRadius + ) + + //如果四个角的值没有设置,那么就使用通用的radius的值。 + if (defaultRadius == leftTopRadius) { + leftTopRadius = radius + } + if (defaultRadius == rightTopRadius) { + rightTopRadius = radius + } + if (defaultRadius == rightBottomRadius) { + rightBottomRadius = radius + } + if (defaultRadius == leftBottomRadius) { + leftBottomRadius = radius + } + array.recycle() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + width = getWidth().toFloat() + height = getHeight().toFloat() + } + + override fun onDraw(canvas: Canvas) { + //这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪 + val maxLeft = max(leftTopRadius, leftBottomRadius) + val maxRight = max(rightTopRadius, rightBottomRadius) + val minWidth = maxLeft + maxRight + val maxTop = max(leftTopRadius, rightTopRadius) + val maxBottom = max(leftBottomRadius, rightBottomRadius) + val minHeight = maxTop + maxBottom + if (width >= minWidth && height > minHeight) { + @SuppressLint("DrawAllocation") val path = Path() + //四个角:右上,右下,左下,左上 + path.moveTo(leftTopRadius.toFloat(), 0f) + path.lineTo(width - rightTopRadius, 0f) + path.quadTo(width, 0f, width, rightTopRadius.toFloat()) + + path.lineTo(width, height - rightBottomRadius) + path.quadTo(width, height, width - rightBottomRadius, height) + + path.lineTo(leftBottomRadius.toFloat(), height) + path.quadTo(0f, height, 0f, height - leftBottomRadius) + + path.lineTo(0f, leftTopRadius.toFloat()) + path.quadTo(0f, 0f, leftTopRadius.toFloat(), 0f) + + canvas.clipPath(path) + } + super.onDraw(canvas) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/image/ImageButton.kt b/app/src/main/java/io/legado/app/ui/widget/image/ImageButton.kt new file mode 100755 index 000000000..e21d00834 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/ImageButton.kt @@ -0,0 +1,20 @@ +package io.legado.app.ui.widget.image + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView + +class ImageButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + + + override fun setEnabled(enabled: Boolean) { + if (isEnabled != enabled) { + imageAlpha = if (enabled) 0xFF else 0x3F + } + super.setEnabled(enabled) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt b/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt new file mode 100755 index 000000000..d3cc1db4f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt @@ -0,0 +1,1260 @@ +package io.legado.app.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.view.ScaleGestureDetector.OnScaleGestureListener +import android.view.View +import android.view.ViewGroup +import android.view.ViewParent +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import android.widget.ImageView +import android.widget.OverScroller +import android.widget.Scroller +import androidx.appcompat.widget.AppCompatImageView +import io.legado.app.ui.widget.image.photo.Info +import io.legado.app.ui.widget.image.photo.OnRotateListener +import io.legado.app.ui.widget.image.photo.RotateGestureDetector +import kotlin.math.abs +import kotlin.math.roundToInt + +@Suppress("UNUSED_PARAMETER", "unused", "MemberVisibilityCanBePrivate", "PropertyName") +class PhotoView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatImageView(context, attrs) { + val MIN_ROTATE = 35 + val ANIMA_DURING = 340 + val MAX_SCALE = 2.5f + + private var mMinRotate = 0 + var mAnimaDuring = 0 + private var mMaxScale = 0f + + var MAX_OVER_SCROLL = 0 + var MAX_FLING_OVER_SCROLL = 0 + var MAX_OVER_RESISTANCE = 0 + var MAX_ANIM_FROM_WAITE = 500 + + private val mBaseMatrix: Matrix = Matrix() + private val mAnimMatrix: Matrix = Matrix() + private val mSynthesisMatrix: Matrix = Matrix() + private val mTmpMatrix: Matrix = Matrix() + + private val mRotateDetector: RotateGestureDetector + private val mDetector: GestureDetector + private val mScaleDetector: ScaleGestureDetector + private var mClickListener: OnClickListener? = null + + private var mScaleType: ScaleType? = null + + private var hasMultiTouch = false + private var hasDrawable = false + private var isKnowSize = false + private var hasOverTranslate = false + + //缩放 + var isEnable = true + + //旋转 + var isRotateEnable = false + private var isInit = false + private var mAdjustViewBounds = false + + // 当前是否处于放大状态 + private var isZoonUp = false + private var canRotate = false + + private var imgLargeWidth = false + private var imgLargeHeight = false + + private var mRotateFlag = 0f + private var mDegrees = 0f + private var mScale = 1.0f + private var mTranslateX = 0 + private var mTranslateY = 0 + + private var mHalfBaseRectWidth = 0f + private var mHalfBaseRectHeight = 0f + + private val mWidgetRect = RectF() + private val mBaseRect = RectF() + private val mImgRect = RectF() + private val mTmpRect = RectF() + private val mCommonRect = RectF() + + private val mScreenCenter = PointF() + private val mScaleCenter = PointF() + private val mRotateCenter = PointF() + + private val mTranslate: Transform = Transform() + + private var mClip: RectF? = null + private var mFromInfo: Info? = null + private var mInfoTime: Long = 0 + private var mCompleteCallBack: Runnable? = null + + private var mLongClick: OnLongClickListener? = null + + private val mRotateListener = RotateListener() + private val mGestureListener = GestureListener() + private val mScaleListener = ScaleGestureListener() + + init { + super.setScaleType(ScaleType.MATRIX) + if (mScaleType == null) mScaleType = ScaleType.CENTER_INSIDE + mRotateDetector = RotateGestureDetector(mRotateListener) + mDetector = GestureDetector(context, mGestureListener) + mScaleDetector = ScaleGestureDetector(context, mScaleListener) + val density = resources.displayMetrics.density + MAX_OVER_SCROLL = (density * 30).toInt() + MAX_FLING_OVER_SCROLL = (density * 30).toInt() + MAX_OVER_RESISTANCE = (density * 140).toInt() + mMinRotate = MIN_ROTATE + mAnimaDuring = ANIMA_DURING + mMaxScale = MAX_SCALE + } + + /** + * 获取默认的动画持续时间 + */ + fun getDefaultAnimDuring(): Int { + return ANIMA_DURING + } + + override fun setOnClickListener(l: OnClickListener?) { + super.setOnClickListener(l) + mClickListener = l + } + + override fun setScaleType(scaleType: ScaleType) { + if (scaleType == ScaleType.MATRIX) return + if (scaleType != mScaleType) { + mScaleType = scaleType + if (isInit) { + initBase() + } + } + } + + override fun setOnLongClickListener(l: OnLongClickListener?) { + mLongClick = l + } + + /** + * 设置动画的插入器 + */ + fun setInterpolator(interpolator: Interpolator?) { + mTranslate.setInterpolator(interpolator) + } + + /** + * 获取动画持续时间 + */ + fun getAnimDuring(): Int { + return mAnimaDuring + } + + /** + * 设置动画的持续时间 + */ + fun setAnimDuring(during: Int) { + mAnimaDuring = during + } + + /** + * 设置最大可以缩放的倍数 + */ + fun setMaxScale(maxScale: Float) { + mMaxScale = maxScale + } + + /** + * 获取最大可以缩放的倍数 + */ + fun getMaxScale(): Float { + return mMaxScale + } + + /** + */ + fun setMaxAnimFromWaiteTime(wait: Int) { + MAX_ANIM_FROM_WAITE = wait + } + + @SuppressLint("UseCompatLoadingForDrawables") + override fun setImageResource(resId: Int) { + val drawable: Drawable? = kotlin.runCatching { + resources.getDrawable(resId, null) + }.getOrNull() + setImageDrawable(drawable) + } + + override fun setImageDrawable(drawable: Drawable?) { + super.setImageDrawable(drawable) + if (drawable == null) { + hasDrawable = false + return + } + if (!hasSize(drawable)) return + if (!hasDrawable) { + hasDrawable = true + } + initBase() + } + + private fun hasSize(d: Drawable): Boolean { + return !((d.intrinsicHeight <= 0 || d.intrinsicWidth <= 0) + && (d.minimumWidth <= 0 || d.minimumHeight <= 0) + && (d.bounds.width() <= 0 || d.bounds.height() <= 0)) + } + + private fun getDrawableWidth(d: Drawable): Int { + var width = d.intrinsicWidth + if (width <= 0) width = d.minimumWidth + if (width <= 0) width = d.bounds.width() + return width + } + + private fun getDrawableHeight(d: Drawable): Int { + var height = d.intrinsicHeight + if (height <= 0) height = d.minimumHeight + if (height <= 0) height = d.bounds.height() + return height + } + + private fun initBase() { + if (!hasDrawable) return + if (!isKnowSize) return + mBaseMatrix.reset() + mAnimMatrix.reset() + isZoonUp = false + val img = drawable + val w = width + val h = height + val imgW = getDrawableWidth(img) + val imgH = getDrawableHeight(img) + mBaseRect[0f, 0f, imgW.toFloat()] = imgH.toFloat() + + // 以图片中心点居中位移 + val tx = (w - imgW) / 2 + val ty = (h - imgH) / 2 + var sx = 1f + var sy = 1f + + // 缩放,默认不超过屏幕大小 + if (imgW > w) { + sx = w.toFloat() / imgW + } + if (imgH > h) { + sy = h.toFloat() / imgH + } + val scale = if (sx < sy) sx else sy + mBaseMatrix.reset() + mBaseMatrix.postTranslate(tx.toFloat(), ty.toFloat()) + mBaseMatrix.postScale(scale, scale, mScreenCenter.x, mScreenCenter.y) + mBaseMatrix.mapRect(mBaseRect) + mHalfBaseRectWidth = mBaseRect.width() / 2 + mHalfBaseRectHeight = mBaseRect.height() / 2 + mScaleCenter.set(mScreenCenter) + mRotateCenter.set(mScaleCenter) + executeTranslate() + when (mScaleType) { + ScaleType.CENTER -> initCenter() + ScaleType.CENTER_CROP -> initCenterCrop() + ScaleType.CENTER_INSIDE -> initCenterInside() + ScaleType.FIT_CENTER -> initFitCenter() + ScaleType.FIT_START -> initFitStart() + ScaleType.FIT_END -> initFitEnd() + ScaleType.FIT_XY -> initFitXY() + else -> { + } + } + isInit = true + mFromInfo?.let { + if (System.currentTimeMillis() - mInfoTime < MAX_ANIM_FROM_WAITE) { + animaFrom(it) + } + } + mFromInfo = null + } + + private fun initCenter() { + if (!hasDrawable) return + if (!isKnowSize) return + val img = drawable + val imgW = getDrawableWidth(img) + val imgH = getDrawableHeight(img) + if (imgW > mWidgetRect.width() || imgH > mWidgetRect.height()) { + val scaleX = imgW / mImgRect.width() + val scaleY = imgH / mImgRect.height() + mScale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initCenterCrop() { + if (mImgRect.width() < mWidgetRect.width() || mImgRect.height() < mWidgetRect.height()) { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mScale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initCenterInside() { + if (mImgRect.width() > mWidgetRect.width() || mImgRect.height() > mWidgetRect.height()) { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mScale = if (scaleX < scaleY) scaleX else scaleY + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initFitCenter() { + if (mImgRect.width() < mWidgetRect.width()) { + mScale = mWidgetRect.width() / mImgRect.width() + mAnimMatrix.postScale(mScale, mScale, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + } + + private fun initFitStart() { + initFitCenter() + val ty = -mImgRect.top + mAnimMatrix.postTranslate(0f, ty) + executeTranslate() + resetBase() + mTranslateY += ty.toInt() + } + + private fun initFitEnd() { + initFitCenter() + val ty = mWidgetRect.bottom - mImgRect.bottom + mTranslateY += ty.toInt() + mAnimMatrix.postTranslate(0f, ty) + executeTranslate() + resetBase() + } + + private fun initFitXY() { + val scaleX = mWidgetRect.width() / mImgRect.width() + val scaleY = mWidgetRect.height() / mImgRect.height() + mAnimMatrix.postScale(scaleX, scaleY, mScreenCenter.x, mScreenCenter.y) + executeTranslate() + resetBase() + } + + private fun resetBase() { + val img = drawable + val imgW = getDrawableWidth(img) + val imgH = getDrawableHeight(img) + mBaseRect[0f, 0f, imgW.toFloat()] = imgH.toFloat() + mBaseMatrix.set(mSynthesisMatrix) + mBaseMatrix.mapRect(mBaseRect) + mHalfBaseRectWidth = mBaseRect.width() / 2 + mHalfBaseRectHeight = mBaseRect.height() / 2 + mScale = 1f + mTranslateX = 0 + mTranslateY = 0 + mAnimMatrix.reset() + } + + private fun executeTranslate() { + mSynthesisMatrix.set(mBaseMatrix) + mSynthesisMatrix.postConcat(mAnimMatrix) + imageMatrix = mSynthesisMatrix + mAnimMatrix.mapRect(mImgRect, mBaseRect) + imgLargeWidth = mImgRect.width() > mWidgetRect.width() + imgLargeHeight = mImgRect.height() > mWidgetRect.height() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (!hasDrawable) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + val d = drawable + val drawableW = getDrawableWidth(d) + val drawableH = getDrawableHeight(d) + val pWidth = MeasureSpec.getSize(widthMeasureSpec) + val pHeight = MeasureSpec.getSize(heightMeasureSpec) + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + var width: Int + var height: Int + var p = layoutParams + if (p == null) { + p = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + width = if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) { + if (widthMode == MeasureSpec.UNSPECIFIED) { + drawableW + } else { + pWidth + } + } else { + if (widthMode == MeasureSpec.EXACTLY) { + pWidth + } else if (widthMode == MeasureSpec.AT_MOST) { + if (drawableW > pWidth) pWidth else drawableW + } else { + drawableW + } + } + height = if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) { + if (heightMode == MeasureSpec.UNSPECIFIED) { + drawableH + } else { + pHeight + } + } else { + if (heightMode == MeasureSpec.EXACTLY) { + pHeight + } else if (heightMode == MeasureSpec.AT_MOST) { + if (drawableH > pHeight) pHeight else drawableH + } else { + drawableH + } + } + if (mAdjustViewBounds && drawableW.toFloat() / drawableH != width.toFloat() / height) { + val hScale = height.toFloat() / drawableH + val wScale = width.toFloat() / drawableW + val scale = if (hScale < wScale) hScale else wScale + width = + if (p.width == ViewGroup.LayoutParams.MATCH_PARENT) width else (drawableW * scale).toInt() + height = + if (p.height == ViewGroup.LayoutParams.MATCH_PARENT) height else (drawableH * scale).toInt() + } + setMeasuredDimension(width, height) + } + + override fun setAdjustViewBounds(adjustViewBounds: Boolean) { + super.setAdjustViewBounds(adjustViewBounds) + mAdjustViewBounds = adjustViewBounds + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mWidgetRect[0f, 0f, w.toFloat()] = h.toFloat() + mScreenCenter[w / 2.toFloat()] = h / 2.toFloat() + if (!isKnowSize) { + isKnowSize = true + initBase() + } + } + + override fun draw(canvas: Canvas) { + mClip?.let { + canvas.clipRect(it) + mClip = null + } + super.draw(canvas) + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return if (isEnable) { + val action = event.actionMasked + if (event.pointerCount >= 2) hasMultiTouch = true + mDetector.onTouchEvent(event) + if (isRotateEnable) { + mRotateDetector.onTouchEvent(event) + } + mScaleDetector.onTouchEvent(event) + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) onUp() + true + } else { + super.dispatchTouchEvent(event) + } + } + + private fun onUp() { + if (mTranslate.isRunning) return + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + var scale = mScale + if (mScale < 1) { + scale = 1f + mTranslate.withScale(mScale, 1F) + } else if (mScale > mMaxScale) { + scale = mMaxScale + mTranslate.withScale(mScale, mMaxScale) + } + val cx = mImgRect.left + mImgRect.width() / 2 + val cy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[cx] = cy + mRotateCenter[cx] = cy + mTranslateX = 0 + mTranslateY = 0 + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(cx - mHalfBaseRectWidth, cy - mHalfBaseRectHeight) + mTmpMatrix.postScale(scale, scale, cx, cy) + mTmpMatrix.postRotate(mDegrees, cx, cy) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + mTranslate.start() + } + + private fun doTranslateReset(imgRect: RectF) { + var tx = 0 + var ty = 0 + if (imgRect.width() <= mWidgetRect.width()) { + if (!isImageCenterWidth(imgRect)) tx = + (-((mWidgetRect.width() - imgRect.width()) / 2 - imgRect.left)).toInt() + } else { + if (imgRect.left > mWidgetRect.left) { + tx = (imgRect.left - mWidgetRect.left).toInt() + } else if (imgRect.right < mWidgetRect.right) { + tx = (imgRect.right - mWidgetRect.right).toInt() + } + } + if (imgRect.height() <= mWidgetRect.height()) { + if (!isImageCenterHeight(imgRect)) ty = + (-((mWidgetRect.height() - imgRect.height()) / 2 - imgRect.top)).toInt() + } else { + if (imgRect.top > mWidgetRect.top) { + ty = (imgRect.top - mWidgetRect.top).toInt() + } else if (imgRect.bottom < mWidgetRect.bottom) { + ty = (imgRect.bottom - mWidgetRect.bottom).toInt() + } + } + if (tx != 0 || ty != 0) { + if (!mTranslate.mFlingScroller.isFinished) mTranslate.mFlingScroller.abortAnimation() + mTranslate.withTranslate(mTranslateX, mTranslateY, -tx, -ty) + } + } + + private fun isImageCenterHeight(rect: RectF): Boolean { + return abs(rect.top.roundToInt() - (mWidgetRect.height() - rect.height()) / 2) < 1 + } + + private fun isImageCenterWidth(rect: RectF): Boolean { + return abs(rect.left.roundToInt() - (mWidgetRect.width() - rect.width()) / 2) < 1 + } + + private fun resistanceScrollByX( + overScroll: Float, + detalX: Float + ): Float { + return detalX * (abs(abs(overScroll) - MAX_OVER_RESISTANCE) / MAX_OVER_RESISTANCE.toFloat()) + } + + private fun resistanceScrollByY( + overScroll: Float, + detalY: Float + ): Float { + return detalY * (abs(abs(overScroll) - MAX_OVER_RESISTANCE) / MAX_OVER_RESISTANCE.toFloat()) + } + + /** + * 匹配两个Rect的共同部分输出到out,若无共同部分则输出0,0,0,0 + */ + private fun mapRect(r1: RectF, r2: RectF, out: RectF) { + val l: Float = if (r1.left > r2.left) r1.left else r2.left + val r: Float = if (r1.right < r2.right) r1.right else r2.right + if (l > r) { + out[0f, 0f, 0f] = 0f + return + } + val t: Float = if (r1.top > r2.top) r1.top else r2.top + val b: Float = if (r1.bottom < r2.bottom) r1.bottom else r2.bottom + if (t > b) { + out[0f, 0f, 0f] = 0f + return + } + out[l, t, r] = b + } + + private fun checkRect() { + if (!hasOverTranslate) { + mapRect(mWidgetRect, mImgRect, mCommonRect) + } + } + + private val mClickRunnable = Runnable { + mClickListener?.onClick(this) + } + + fun canScrollHorizontallySelf(direction: Float): Boolean { + if (mImgRect.width() <= mWidgetRect.width()) + return false + if (direction < 0 && mImgRect.left.roundToInt() - direction >= mWidgetRect.left) + return false + return !(direction > 0 && mImgRect.right.roundToInt() - direction <= mWidgetRect.right) + } + + fun canScrollVerticallySelf(direction: Float): Boolean { + if (mImgRect.height() <= mWidgetRect.height()) + return false + if (direction < 0 && mImgRect.top.roundToInt() - direction >= mWidgetRect.top) + return false + return !(direction > 0 && mImgRect.bottom.roundToInt() - direction <= mWidgetRect.bottom) + } + + override fun canScrollHorizontally(direction: Int): Boolean { + return if (hasMultiTouch) true else canScrollHorizontallySelf(direction.toFloat()) + } + + override fun canScrollVertically(direction: Int): Boolean { + return if (hasMultiTouch) true else canScrollVerticallySelf(direction.toFloat()) + } + + private inner class InterpolatorProxy : Interpolator { + private var mTarget: Interpolator? + + init { + mTarget = DecelerateInterpolator() + } + + fun setTargetInterpolator(interpolator: Interpolator?) { + mTarget = interpolator + } + + override fun getInterpolation(input: Float): Float { + return mTarget?.getInterpolation(input) ?: input + } + + } + + private inner class Transform : Runnable { + var isRunning = false + var mTranslateScroller: OverScroller + var mFlingScroller: OverScroller + var mScaleScroller: Scroller + var mClipScroller: Scroller + var mRotateScroller: Scroller + var c: ClipCalculate? = null + var mLastFlingX = 0 + var mLastFlingY = 0 + var mLastTranslateX = 0 + var mLastTranslateY = 0 + var mClipRect = RectF() + var mInterpolatorProxy = InterpolatorProxy() + + fun setInterpolator(interpolator: Interpolator?) { + mInterpolatorProxy.setTargetInterpolator(interpolator) + } + + init { + val ctx: Context = context + mTranslateScroller = OverScroller(ctx, mInterpolatorProxy) + mScaleScroller = Scroller(ctx, mInterpolatorProxy) + mFlingScroller = OverScroller(ctx, mInterpolatorProxy) + mClipScroller = Scroller(ctx, mInterpolatorProxy) + mRotateScroller = Scroller(ctx, mInterpolatorProxy) + } + + fun withTranslate(startX: Int, startY: Int, deltaX: Int, deltaY: Int) { + mLastTranslateX = 0 + mLastTranslateY = 0 + mTranslateScroller.startScroll(0, 0, deltaX, deltaY, mAnimaDuring) + } + + fun withScale(form: Float, to: Float) { + mScaleScroller.startScroll( + (form * 10000).toInt(), + 0, + ((to - form) * 10000).toInt(), + 0, + mAnimaDuring + ) + } + + fun withClip( + fromX: Float, + fromY: Float, + deltaX: Float, + deltaY: Float, + d: Int, + c: ClipCalculate? + ) { + mClipScroller.startScroll( + (fromX * 10000).toInt(), + (fromY * 10000).toInt(), + (deltaX * 10000).toInt(), + (deltaY * 10000).toInt(), + d + ) + this.c = c + } + + fun withRotate(fromDegrees: Int, toDegrees: Int) { + mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, mAnimaDuring) + } + + fun withRotate(fromDegrees: Int, toDegrees: Int, during: Int) { + mRotateScroller.startScroll(fromDegrees, 0, toDegrees - fromDegrees, 0, during) + } + + fun withFling(velocityX: Float, velocityY: Float) { + mLastFlingX = if (velocityX < 0) Int.MAX_VALUE else 0 + var distanceX = + (if (velocityX > 0) abs(mImgRect.left) else mImgRect.right - mWidgetRect.right).toInt() + distanceX = if (velocityX < 0) Int.MAX_VALUE - distanceX else distanceX + var minX = if (velocityX < 0) distanceX else 0 + var maxX = if (velocityX < 0) Int.MAX_VALUE else distanceX + val overX = if (velocityX < 0) Int.MAX_VALUE - minX else distanceX + mLastFlingY = if (velocityY < 0) Int.MAX_VALUE else 0 + var distanceY = + (if (velocityY > 0) abs(mImgRect.top) else mImgRect.bottom - mWidgetRect.bottom).toInt() + distanceY = if (velocityY < 0) Int.MAX_VALUE - distanceY else distanceY + var minY = if (velocityY < 0) distanceY else 0 + var maxY = if (velocityY < 0) Int.MAX_VALUE else distanceY + val overY = if (velocityY < 0) Int.MAX_VALUE - minY else distanceY + if (velocityX == 0f) { + maxX = 0 + minX = 0 + } + if (velocityY == 0f) { + maxY = 0 + minY = 0 + } + mFlingScroller.fling( + mLastFlingX, + mLastFlingY, + velocityX.toInt(), + velocityY.toInt(), + minX, + maxX, + minY, + maxY, + if (abs(overX) < MAX_FLING_OVER_SCROLL * 2) 0 else MAX_FLING_OVER_SCROLL, + if (abs(overY) < MAX_FLING_OVER_SCROLL * 2) 0 else MAX_FLING_OVER_SCROLL + ) + } + + fun start() { + isRunning = true + postExecute() + } + + fun stop() { + removeCallbacks(this) + mTranslateScroller.abortAnimation() + mScaleScroller.abortAnimation() + mFlingScroller.abortAnimation() + mRotateScroller.abortAnimation() + isRunning = false + } + + override fun run() { + + // if (!isRuning) return; + var endAnima = true + if (mScaleScroller.computeScrollOffset()) { + mScale = mScaleScroller.currX / 10000f + endAnima = false + } + if (mTranslateScroller.computeScrollOffset()) { + val tx = mTranslateScroller.currX - mLastTranslateX + val ty = mTranslateScroller.currY - mLastTranslateY + mTranslateX += tx + mTranslateY += ty + mLastTranslateX = mTranslateScroller.currX + mLastTranslateY = mTranslateScroller.currY + endAnima = false + } + if (mFlingScroller.computeScrollOffset()) { + val x = mFlingScroller.currX - mLastFlingX + val y = mFlingScroller.currY - mLastFlingY + mLastFlingX = mFlingScroller.currX + mLastFlingY = mFlingScroller.currY + mTranslateX += x + mTranslateY += y + endAnima = false + } + if (mRotateScroller.computeScrollOffset()) { + mDegrees = mRotateScroller.currX.toFloat() + endAnima = false + } + if (mClipScroller.computeScrollOffset() || mClip != null) { + val sx = mClipScroller.currX / 10000f + val sy = mClipScroller.currY / 10000f + mTmpMatrix.setScale( + sx, + sy, + (mImgRect.left + mImgRect.right) / 2, + c!!.calculateTop() + ) + mTmpMatrix.mapRect(mClipRect, mImgRect) + if (sx == 1f) { + mClipRect.left = mWidgetRect.left + mClipRect.right = mWidgetRect.right + } + if (sy == 1f) { + mClipRect.top = mWidgetRect.top + mClipRect.bottom = mWidgetRect.bottom + } + mClip = mClipRect + } + if (!endAnima) { + applyAnima() + postExecute() + } else { + isRunning = false + + // 修复动画结束后边距有些空隙, + var needFix = false + if (imgLargeWidth) { + if (mImgRect.left > 0) { + mTranslateX -= mImgRect.left.toInt() + } else if (mImgRect.right < mWidgetRect.width()) { + mTranslateX -= (mWidgetRect.width() - mImgRect.right).toInt() + } + needFix = true + } + if (imgLargeHeight) { + if (mImgRect.top > 0) { + mTranslateY -= mImgRect.top.toInt() + } else if (mImgRect.bottom < mWidgetRect.height()) { + mTranslateY -= (mWidgetRect.height() - mImgRect.bottom).toInt() + } + needFix = true + } + if (needFix) { + applyAnima() + } + invalidate() + mCompleteCallBack?.let { + it.run() + mCompleteCallBack = null + } + } + } + + private fun applyAnima() { + mAnimMatrix.reset() + mAnimMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mAnimMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mAnimMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mAnimMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mAnimMatrix.postScale(mScale, mScale, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + executeTranslate() + } + + private fun postExecute() { + if (isRunning) post(this) + } + + } + + fun getInfo(): Info { + val rect = RectF() + val p = IntArray(2) + getLocation(this, p) + rect[p[0] + mImgRect.left, p[1] + mImgRect.top, p[0] + mImgRect.right] = + p[1] + mImgRect.bottom + return Info( + rect, + mImgRect, + mWidgetRect, + mBaseRect, + mScreenCenter, + mScale, + mDegrees, + mScaleType + ) + } + + fun getImageViewInfo(imgView: ImageView): Info { + val p = IntArray(2) + getLocation(imgView, p) + val drawable: Drawable = imgView.drawable + val matrix: Matrix = imgView.imageMatrix + val width = getDrawableWidth(drawable) + val height = getDrawableHeight(drawable) + val imgRect = RectF(0F, 0F, width.toFloat(), height.toFloat()) + matrix.mapRect(imgRect) + val rect = RectF( + p[0] + imgRect.left, + p[1] + imgRect.top, + p[0] + imgRect.right, + p[1] + imgRect.bottom + ) + val widgetRect = RectF(0F, 0F, imgView.width.toFloat(), imgView.height.toFloat()) + val baseRect = RectF(widgetRect) + val screenCenter = PointF(widgetRect.width() / 2, widgetRect.height() / 2) + return Info( + rect, + imgRect, + widgetRect, + baseRect, + screenCenter, + 1F, + 0F, + imgView.scaleType + ) + } + + private fun getLocation(target: View, position: IntArray) { + position[0] += target.left + position[1] += target.top + var viewParent: ViewParent = target.parent + while (viewParent is View) { + val view: View = viewParent + if (view.id == android.R.id.content) return + position[0] -= view.scrollX + position[1] -= view.scrollY + position[0] += view.left + position[1] += view.top + viewParent = view.parent + } + position[0] = (position[0] + 0.5f).toInt() + position[1] = (position[1] + 0.5f).toInt() + } + + private fun reset() { + mAnimMatrix.reset() + executeTranslate() + mScale = 1f + mTranslateX = 0 + mTranslateY = 0 + } + + interface ClipCalculate { + fun calculateTop(): Float + } + + inner class START : ClipCalculate { + override fun calculateTop(): Float { + return mImgRect.top + } + } + + inner class END : ClipCalculate { + override fun calculateTop(): Float { + return mImgRect.bottom + } + } + + inner class OTHER : ClipCalculate { + override fun calculateTop(): Float { + return (mImgRect.top + mImgRect.bottom) / 2 + } + } + + /** + * 在PhotoView内部还没有图片的时候同样可以调用该方法 + * + * + * 此时并不会播放动画,当给PhotoView设置图片后会自动播放动画。 + * + * + * 若等待时间过长也没有给控件设置图片,则会忽略该动画,若要再次播放动画则需要重新调用该方法 + * (等待的时间默认500毫秒,可以通过setMaxAnimFromWaiteTime(int)设置最大等待时间) + */ + fun animaFrom(info: Info) { + if (isInit) { + reset() + val mine = getInfo() + val scaleX = info.mImgRect.width() / mine.mImgRect.width() + val scaleY = info.mImgRect.height() / mine.mImgRect.height() + val scale = if (scaleX < scaleY) scaleX else scaleY + val ocx = info.mRect.left + info.mRect.width() / 2 + val ocy = info.mRect.top + info.mRect.height() / 2 + val mcx = mine.mRect.left + mine.mRect.width() / 2 + val mcy = mine.mRect.top + mine.mRect.height() / 2 + mAnimMatrix.reset() + // mAnimaMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top); + mAnimMatrix.postTranslate(ocx - mcx, ocy - mcy) + mAnimMatrix.postScale(scale, scale, ocx, ocy) + mAnimMatrix.postRotate(info.mDegrees, ocx, ocy) + executeTranslate() + mScaleCenter[ocx] = ocy + mRotateCenter[ocx] = ocy + mTranslate.withTranslate(0, 0, (-(ocx - mcx)).toInt(), (-(ocy - mcy)).toInt()) + mTranslate.withScale(scale, 1F) + mTranslate.withRotate(info.mDegrees.toInt(), 0) + if (info.mWidgetRect.width() < info.mImgRect.width() || info.mWidgetRect.height() < info.mImgRect.height()) { + var clipX = info.mWidgetRect.width() / info.mImgRect.width() + var clipY = info.mWidgetRect.height() / info.mImgRect.height() + clipX = if (clipX > 1) 1F else clipX + clipY = if (clipY > 1) 1F else clipY + val c = + if (info.mScaleType == ScaleType.FIT_START) START() else if (info.mScaleType == ScaleType.FIT_END) END() else OTHER() + mTranslate.withClip(clipX, clipY, 1 - clipX, 1 - clipY, mAnimaDuring / 3, c) + mTmpMatrix.setScale( + clipX, + clipY, + (mImgRect.left + mImgRect.right) / 2, + c.calculateTop() + ) + mTmpMatrix.mapRect(mTranslate.mClipRect, mImgRect) + mClip = mTranslate.mClipRect + } + mTranslate.start() + } else { + mFromInfo = info + mInfoTime = System.currentTimeMillis() + } + } + + fun animaTo( + info: Info, + completeCallBack: Runnable + ) { + if (isInit) { + mTranslate.stop() + mTranslateX = 0 + mTranslateY = 0 + val tcx = info.mRect.left + info.mRect.width() / 2 + val tcy = info.mRect.top + info.mRect.height() / 2 + mScaleCenter[mImgRect.left + mImgRect.width() / 2] = + mImgRect.top + mImgRect.height() / 2 + mRotateCenter.set(mScaleCenter) + + // 将图片旋转回正常位置,用以计算 + mAnimMatrix.postRotate(-mDegrees, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.mapRect(mImgRect, mBaseRect) + + // 缩放 + val scaleX = info.mImgRect.width() / mBaseRect.width() + val scaleY = info.mImgRect.height() / mBaseRect.height() + val scale = if (scaleX > scaleY) scaleX else scaleY + mAnimMatrix.postRotate(mDegrees, mScaleCenter.x, mScaleCenter.y) + mAnimMatrix.mapRect(mImgRect, mBaseRect) + mDegrees %= 360 + mTranslate.withTranslate( + 0, + 0, + (tcx - mScaleCenter.x).toInt(), + (tcy - mScaleCenter.y).toInt() + ) + mTranslate.withScale(mScale, scale) + mTranslate.withRotate(mDegrees.toInt(), info.mDegrees.toInt(), mAnimaDuring * 2 / 3) + if (info.mWidgetRect.width() < info.mRect.width() || info.mWidgetRect.height() < info.mRect.height()) { + var clipX = info.mWidgetRect.width() / info.mRect.width() + var clipY = info.mWidgetRect.height() / info.mRect.height() + clipX = if (clipX > 1) 1F else clipX + clipY = if (clipY > 1) 1F else clipY + val cx = clipX + val cy = clipY + val c = + if (info.mScaleType == ScaleType.FIT_START) START() else if (info.mScaleType == ScaleType.FIT_END) END() else OTHER() + postDelayed( + { mTranslate.withClip(1F, 1F, -1 + cx, -1 + cy, mAnimaDuring / 2, c) }, + mAnimaDuring / 2.toLong() + ) + } + mCompleteCallBack = completeCallBack + mTranslate.start() + } + } + + fun rotate(degrees: Float) { + mDegrees += degrees + val centerX = (mWidgetRect.left + mWidgetRect.width() / 2).toInt() + val centerY = (mWidgetRect.top + mWidgetRect.height() / 2).toInt() + mAnimMatrix.postRotate(degrees, centerX.toFloat(), centerY.toFloat()) + executeTranslate() + } + + inner class RotateListener : OnRotateListener { + override fun onRotate( + degrees: Float, + focusX: Float, + focusY: Float + ) { + mRotateFlag += degrees + if (canRotate) { + mDegrees += degrees + mAnimMatrix.postRotate(degrees, focusX, focusY) + } else { + if (abs(mRotateFlag) >= mMinRotate) { + canRotate = true + mRotateFlag = 0f + } + } + } + } + + inner class GestureListener : SimpleOnGestureListener() { + override fun onLongPress(e: MotionEvent) { + mLongClick?.onLongClick(this@PhotoView) + } + + override fun onDown(e: MotionEvent): Boolean { + hasOverTranslate = false + hasMultiTouch = false + canRotate = false + removeCallbacks(mClickRunnable) + return false + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (hasMultiTouch) return false + if (!imgLargeWidth && !imgLargeHeight) return false + if (mTranslate.isRunning) return false + var vx = velocityX + var vy = velocityY + if (mImgRect.left.roundToInt() >= mWidgetRect.left + || mImgRect.right.roundToInt() <= mWidgetRect.right + ) { + vx = 0f + } + if (mImgRect.top.roundToInt() >= mWidgetRect.top + || mImgRect.bottom.roundToInt() <= mWidgetRect.bottom + ) { + vy = 0f + } + if (canRotate || mDegrees % 90 != 0f) { + var toDegrees = (mDegrees / 90).toInt() * 90.toFloat() + val remainder = mDegrees % 90 + if (remainder > 45) toDegrees += 90f else if (remainder < -45) toDegrees -= 90f + mTranslate.withRotate(mDegrees.toInt(), toDegrees.toInt()) + mDegrees = toDegrees + } + doTranslateReset(mImgRect) + mTranslate.withFling(vx, vy) + mTranslate.start() + // onUp(e2); + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + var x = distanceX + var y = distanceY + if (mTranslate.isRunning) { + mTranslate.stop() + } + if (canScrollHorizontallySelf(x)) { + if (x < 0 && mImgRect.left - x > mWidgetRect.left) + x = mImgRect.left + if (x > 0 && mImgRect.right - x < mWidgetRect.right) + x = mImgRect.right - mWidgetRect.right + mAnimMatrix.postTranslate(-x, 0f) + mTranslateX -= x.toInt() + } else if (imgLargeWidth || hasMultiTouch || hasOverTranslate) { + checkRect() + if (!hasMultiTouch) { + if (x < 0 && mImgRect.left - x > mCommonRect.left) x = + resistanceScrollByX(mImgRect.left - mCommonRect.left, x) + if (x > 0 && mImgRect.right - x < mCommonRect.right) x = + resistanceScrollByX(mImgRect.right - mCommonRect.right, x) + } + mTranslateX -= x.toInt() + mAnimMatrix.postTranslate(-x, 0f) + hasOverTranslate = true + } + if (canScrollVerticallySelf(y)) { + if (y < 0 && mImgRect.top - y > mWidgetRect.top) y = + mImgRect.top + if (y > 0 && mImgRect.bottom - y < mWidgetRect.bottom) y = + mImgRect.bottom - mWidgetRect.bottom + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + } else if (imgLargeHeight || hasOverTranslate || hasMultiTouch) { + checkRect() + if (!hasMultiTouch) { + if (y < 0 && mImgRect.top - y > mCommonRect.top) y = + resistanceScrollByY(mImgRect.top - mCommonRect.top, y) + if (y > 0 && mImgRect.bottom - y < mCommonRect.bottom) y = + resistanceScrollByY(mImgRect.bottom - mCommonRect.bottom, y) + } + mAnimMatrix.postTranslate(0f, -y) + mTranslateY -= y.toInt() + hasOverTranslate = true + } + executeTranslate() + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + postDelayed(mClickRunnable, 250) + return false + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + mTranslate.stop() + val from: Float + val to: Float + val imgCx = mImgRect.left + mImgRect.width() / 2 + val imgCy = mImgRect.top + mImgRect.height() / 2 + mScaleCenter[imgCx] = imgCy + mRotateCenter[imgCx] = imgCy + mTranslateX = 0 + mTranslateY = 0 + if (isZoonUp) { + from = mScale + to = 1f + } else { + from = mScale + to = mMaxScale + mScaleCenter[e.x] = e.y + } + mTmpMatrix.reset() + mTmpMatrix.postTranslate(-mBaseRect.left, -mBaseRect.top) + mTmpMatrix.postTranslate(mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postTranslate(-mHalfBaseRectWidth, -mHalfBaseRectHeight) + mTmpMatrix.postRotate(mDegrees, mRotateCenter.x, mRotateCenter.y) + mTmpMatrix.postScale(to, to, mScaleCenter.x, mScaleCenter.y) + mTmpMatrix.postTranslate(mTranslateX.toFloat(), mTranslateY.toFloat()) + mTmpMatrix.mapRect(mTmpRect, mBaseRect) + doTranslateReset(mTmpRect) + isZoonUp = !isZoonUp + mTranslate.withScale(from, to) + mTranslate.start() + return false + } + } + + inner class ScaleGestureListener : OnScaleGestureListener { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scaleFactor = detector.scaleFactor + if (java.lang.Float.isNaN(scaleFactor) || java.lang.Float.isInfinite(scaleFactor)) return false + mScale *= scaleFactor + //mScaleCenter.set(detector.getFocusX(), detector.getFocusY()); + mAnimMatrix.postScale( + scaleFactor, + scaleFactor, + detector.focusX, + detector.focusY + ) + executeTranslate() + return true + } + + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) {} + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt new file mode 100755 index 000000000..6ce1adfc6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/Info.kt @@ -0,0 +1,49 @@ +package io.legado.app.ui.widget.image.photo + +import android.graphics.PointF + +import android.graphics.RectF +import android.widget.ImageView + + +@Suppress("MemberVisibilityCanBePrivate") +class Info( + rect: RectF, + img: RectF, + widget: RectF, + base: RectF, + screenCenter: PointF, + scale: Float, + degrees: Float, + scaleType: ImageView.ScaleType? +) { + // 内部图片在整个手机界面的位置 + var mRect = RectF() + + // 控件在窗口的位置 + var mImgRect = RectF() + + var mWidgetRect = RectF() + + var mBaseRect = RectF() + + var mScreenCenter = PointF() + + var mScale = 0f + + var mDegrees = 0f + + var mScaleType: ImageView.ScaleType? = null + + init { + mRect.set(rect) + mImgRect.set(img) + mWidgetRect.set(widget) + mScale = scale + mScaleType = scaleType + mDegrees = degrees + mBaseRect.set(base) + mScreenCenter.set(screenCenter) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt new file mode 100755 index 000000000..7dddbebbf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/photo/RotateGestureDetector.kt @@ -0,0 +1,54 @@ +package io.legado.app.ui.widget.image.photo + +import android.view.MotionEvent +import kotlin.math.abs +import kotlin.math.atan + +class RotateGestureDetector(private val mListener: OnRotateListener) { + + private val MAX_DEGREES_STEP = 120 + + private var mPrevSlope = 0f + private var mCurrSlope = 0f + + private val x1 = 0f + private val y1 = 0f + private val x2 = 0f + private val y2 = 0f + + fun onTouchEvent(event: MotionEvent) { + + when (event.actionMasked) { + MotionEvent.ACTION_POINTER_DOWN, + MotionEvent.ACTION_POINTER_UP -> { + if (event.pointerCount == 2) mPrevSlope = calculateSlope(event) + } + MotionEvent.ACTION_MOVE -> if (event.pointerCount > 1) { + mCurrSlope = calculateSlope(event) + + val currDegrees = Math.toDegrees(atan(mCurrSlope.toDouble())) + val prevDegrees = Math.toDegrees(atan(mPrevSlope.toDouble())) + + val deltaSlope = currDegrees - prevDegrees + + if (abs(deltaSlope) <= MAX_DEGREES_STEP) { + mListener.onRotate(deltaSlope.toFloat(), (x2 + x1) / 2, (y2 + y1) / 2) + } + mPrevSlope = mCurrSlope + } + } + + } + + private fun calculateSlope(event: MotionEvent): Float { + val x1 = event.getX(0) + val y1 = event.getY(0) + val x2 = event.getX(1) + val y2 = event.getY(1) + return (y2 - y1) / (x2 - x1) + } +} + +interface OnRotateListener { + fun onRotate(degrees: Float, focusX: Float, focusY: Float) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt b/app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt new file mode 100755 index 000000000..720cd8ceb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt @@ -0,0 +1,181 @@ +package io.legado.app.ui.widget.keyboard + +import android.content.Context +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.core.view.setPadding +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.KeyboardAssist +import io.legado.app.databinding.DialogMultipleEditTextBinding +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.Item1lineTextAndDelBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.recycler.ItemTouchCallback +import io.legado.app.ui.widget.recycler.VerticalDivider +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +/** + * 辅助按键配置 + */ +class KeyboardAssistsConfig : BaseDialogFragment(R.layout.dialog_recycler_view), + Toolbar.OnMenuItemClickListener { + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val adapter by lazy { KeyAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(0.9f, 0.9f) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.assists_key_config) + initView() + initMenu() + initData() + } + + private fun initView() { + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.addItemDecoration(VerticalDivider(requireContext())) + binding.recyclerView.adapter = adapter + val itemTouchCallback = ItemTouchCallback(adapter) + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView) + + } + + private fun initMenu() { + binding.toolBar.setOnMenuItemClickListener(this) + binding.toolBar.inflateMenu(R.menu.keyboard_assists_config) + binding.toolBar.menu.applyTint(requireContext()) + } + + private fun initData() { + lifecycleScope.launch { + appDb.keyboardAssistsDao.flowAll.catch { + AppLog.put("辅助按键配置获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> editKey(null) + } + return false + } + + private fun editKey(keyboardAssist: KeyboardAssist?) { + alert { + setTitle("辅助按键") + val alertBinding = DialogMultipleEditTextBinding.inflate(layoutInflater).apply { + layout1.hint = "key" + edit1.setText(keyboardAssist?.key) + layout2.hint = "value" + layout2.visible() + edit2.setText(keyboardAssist?.value) + } + setCustomView(alertBinding.root) + cancelButton() + okButton { + lifecycleScope.launch(IO) { + val newKeyboardAssist = KeyboardAssist( + key = alertBinding.edit1.text.toString(), + value = alertBinding.edit2.text.toString() + ) + if (keyboardAssist == null) { + newKeyboardAssist.serialNo = appDb.keyboardAssistsDao.maxSerialNo + 1 + appDb.keyboardAssistsDao.insert(newKeyboardAssist) + } else { + newKeyboardAssist.serialNo = keyboardAssist.serialNo + appDb.keyboardAssistsDao.delete(keyboardAssist) + appDb.keyboardAssistsDao.insert(newKeyboardAssist) + } + } + } + } + } + + private inner class KeyAdapter(context: Context) : + RecyclerAdapter(context), + ItemTouchCallback.Callback { + + private var isMoved = false + + override fun getViewBinding(parent: ViewGroup): Item1lineTextAndDelBinding { + return Item1lineTextAndDelBinding.inflate(inflater, parent, false).apply { + root.setPadding(16.dpToPx()) + ivDelete.visible() + } + } + + override fun convert( + holder: ItemViewHolder, + binding: Item1lineTextAndDelBinding, + item: KeyboardAssist, + payloads: MutableList + ) { + binding.root.setBackgroundColor(context.backgroundColor) + binding.textView.text = item.key + } + + override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextAndDelBinding) { + binding.root.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { keyboardAssist -> + editKey(keyboardAssist) + } + } + binding.ivDelete.setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { keyboardAssist -> + lifecycleScope.launch(IO) { + appDb.keyboardAssistsDao.delete(keyboardAssist) + } + } + } + } + + override fun swap(srcPosition: Int, targetPosition: Int): Boolean { + swapItem(srcPosition, targetPosition) + isMoved = true + return true + } + + override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.serialNo = index + 1 + } + lifecycleScope.launch(IO) { + appDb.keyboardAssistsDao.update(*getItems().toTypedArray()) + } + } + isMoved = false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt b/app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt new file mode 100755 index 000000000..62739bfd0 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt @@ -0,0 +1,172 @@ +package io.legado.app.ui.widget.keyboard + +import android.content.Context +import android.graphics.Rect +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.view.Window +import android.widget.PopupWindow +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.KeyboardAssist +import io.legado.app.databinding.ItemFilletTextBinding +import io.legado.app.databinding.PopupKeyboardToolBinding +import io.legado.app.lib.dialogs.SelectItem +import io.legado.app.lib.dialogs.selector +import io.legado.app.utils.activity +import io.legado.app.utils.showDialogFragment +import io.legado.app.utils.windowSize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import splitties.systemservices.layoutInflater +import splitties.systemservices.windowManager +import kotlin.math.abs + +/** + * 键盘帮助浮窗 + */ +class KeyboardToolPop( + private val context: Context, + private val scope: CoroutineScope, + private val rootView: View, + private val callBack: CallBack +) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT), + ViewTreeObserver.OnGlobalLayoutListener { + + private val helpChar = "❓" + + private val binding = PopupKeyboardToolBinding.inflate(LayoutInflater.from(context)) + private val adapter = Adapter(context) + private var mIsSoftKeyBoardShowing = false + var initialPadding = 0 + + init { + contentView = binding.root + + isTouchable = true + isOutsideTouchable = false + isFocusable = false + inputMethodMode = INPUT_METHOD_NEEDED //解决遮盖输入法 + initRecyclerView() + upAdapterData() + } + + fun attachToWindow(window: Window) { + window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this) + contentView.measure( + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.UNSPECIFIED, + ) + } + + override fun onGlobalLayout() { + val rect = Rect() + // 获取当前页面窗口的显示范围 + rootView.getWindowVisibleDisplayFrame(rect) + val screenHeight = windowManager.windowSize.heightPixels + val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 + val preShowing = mIsSoftKeyBoardShowing + if (abs(keyboardHeight) > screenHeight / 5) { + mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 + rootView.setPadding(0, 0, 0, initialPadding + contentView.measuredHeight) + if (!isShowing) { + showAtLocation(rootView, Gravity.BOTTOM, 0, 0) + } + } else { + mIsSoftKeyBoardShowing = false + rootView.setPadding(0, 0, 0, 0) + if (preShowing) { + dismiss() + } + } + } + + private fun initRecyclerView() { + binding.recyclerView.adapter = adapter + adapter.addHeaderView { + ItemFilletTextBinding.inflate(context.layoutInflater, it, false).apply { + textView.text = helpChar + root.setOnClickListener { + helpAlert() + } + } + } + } + + @Suppress("MemberVisibilityCanBePrivate") + fun upAdapterData() { + scope.launch { + appDb.keyboardAssistsDao.flowByType(0).catch { + AppLog.put("键盘帮助浮窗获取数据失败\n${it.localizedMessage}", it) + }.flowOn(IO).collect { + adapter.setItems(it) + } + } + } + + private fun helpAlert() { + val items = arrayListOf( + SelectItem(context.getString(R.string.assists_key_config), "keyConfig") + ) + items.addAll(callBack.helpActions()) + context.selector(context.getString(R.string.help), items) { _, selectItem, _ -> + when (selectItem.value) { + "keyConfig" -> config() + else -> callBack.onHelpActionSelect(selectItem.value) + } + } + } + + private fun config() { + contentView.activity?.showDialogFragment() + } + + inner class Adapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemFilletTextBinding { + return ItemFilletTextBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemFilletTextBinding, + item: KeyboardAssist, + payloads: MutableList + ) { + binding.run { + textView.text = item.key + } + } + + override fun registerListener(holder: ItemViewHolder, binding: ItemFilletTextBinding) { + holder.itemView.apply { + setOnClickListener { + getItemByLayoutPosition(holder.layoutPosition)?.let { + callBack.sendText(it.value) + } + } + } + } + } + + interface CallBack { + + fun helpActions(): List> = arrayListOf() + + fun onHelpActionSelect(action: String) + + fun sendText(text: String) + + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt b/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt new file mode 100755 index 000000000..01e11efad --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt @@ -0,0 +1,76 @@ +package io.legado.app.ui.widget.number + +import android.content.Context +import android.widget.NumberPicker +import androidx.appcompat.app.AlertDialog +import io.legado.app.R +import io.legado.app.utils.applyTint +import io.legado.app.utils.hideSoftInput + + +class NumberPickerDialog(context: Context) { + private val builder = AlertDialog.Builder(context) + private var numberPicker: NumberPicker? = null + private var maxValue: Int? = null + private var minValue: Int? = null + private var value: Int? = null + + init { + builder.setView(R.layout.dialog_number_picker) + } + + fun setTitle(title: String): NumberPickerDialog { + builder.setTitle(title) + return this + } + + fun setMaxValue(value: Int): NumberPickerDialog { + maxValue = value + return this + } + + fun setMinValue(value: Int): NumberPickerDialog { + minValue = value + return this + } + + fun setValue(value: Int): NumberPickerDialog { + this.value = value + return this + } + + fun setCustomButton(textId: Int, listener: (() -> Unit)?): NumberPickerDialog { + builder.setNeutralButton(textId) { _, _ -> + numberPicker?.let { + it.clearFocus() + it.hideSoftInput() + listener?.invoke() + } + } + return this + } + + fun show(callBack: ((value: Int) -> Unit)?) { + builder.setPositiveButton(R.string.ok) { _, _ -> + numberPicker?.let { + it.clearFocus() + it.hideSoftInput() + callBack?.invoke(it.value) + } + } + builder.setNegativeButton(R.string.cancel, null) + val dialog = builder.show().applyTint() + numberPicker = dialog.findViewById(R.id.number_picker) + numberPicker?.let { np -> + minValue?.let { + np.minValue = it + } + maxValue?.let { + np.maxValue = it + } + value?.let { + np.value = it + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt new file mode 100755 index 000000000..9ac09b0cf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/DividerNoLast.kt @@ -0,0 +1,170 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.utils.DebugLog + +import kotlin.math.roundToInt + + +/** + * 不画最后一条分隔线 + */ +@Suppress("MemberVisibilityCanBePrivate", "RedundantRequireNotNullCall", "unused") +class DividerNoLast(context: Context, orientation: Int) : + RecyclerView.ItemDecoration() { + + companion object { + const val HORIZONTAL = LinearLayout.HORIZONTAL + const val VERTICAL = LinearLayout.VERTICAL + } + + private val attrs = intArrayOf(android.R.attr.listDivider) + + private var mDivider: Drawable? = null + + /** + * Current orientation. Either [.HORIZONTAL] or [.VERTICAL]. + */ + private var mOrientation = 0 + + private val mBounds = Rect() + + init { + val a = context.obtainStyledAttributes(attrs) + mDivider = a.getDrawable(0) + if (mDivider == null) { + DebugLog.w( + javaClass.name, + "@android:attr/listDivider was not set in the theme used for this DividerItemDecoration. Please set that attribute all call setDrawable()" + ) + } + a.recycle() + setOrientation(orientation) + } + + /** + * Sets the orientation for this divider. This should be called if + * [RecyclerView.LayoutManager] changes orientation. + * + * @param orientation [.HORIZONTAL] or [.VERTICAL] + */ + fun setOrientation(orientation: Int) { + require(!(orientation != HORIZONTAL && orientation != VERTICAL)) { "Invalid orientation. It should be either HORIZONTAL or VERTICAL" } + mOrientation = orientation + } + + /** + * Sets the [Drawable] for this divider. + * + * @param drawable Drawable that should be used as a divider. + */ + fun setDrawable(drawable: Drawable) { + requireNotNull(drawable) { "Drawable cannot be null." } + mDivider = drawable + } + + /** + * @return the [Drawable] for this divider. + */ + fun getDrawable(): Drawable? { + return mDivider + } + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.layoutManager == null || mDivider == null) { + return + } + if (mOrientation == VERTICAL) { + drawVertical(c, parent) + } else { + drawHorizontal(c, parent) + } + } + + private fun drawVertical( + canvas: Canvas, + parent: RecyclerView + ) { + canvas.save() + val left: Int + val right: Int + if (parent.clipToPadding) { + left = parent.paddingLeft + right = parent.width - parent.paddingRight + canvas.clipRect( + left, parent.paddingTop, right, + parent.height - parent.paddingBottom + ) + } else { + left = 0 + right = parent.width + } + val childCount = parent.childCount + for (i in 0 until childCount - 1) { + val child = parent.getChildAt(i) + parent.getDecoratedBoundsWithMargins(child, mBounds) + val bottom = mBounds.bottom + child.translationY.roundToInt() + val top = bottom - mDivider!!.intrinsicHeight + mDivider!!.setBounds(left, top, right, bottom) + mDivider!!.draw(canvas) + } + canvas.restore() + } + + private fun drawHorizontal(canvas: Canvas, parent: RecyclerView) { + canvas.save() + val top: Int + val bottom: Int + if (parent.clipToPadding) { + top = parent.paddingTop + bottom = parent.height - parent.paddingBottom + canvas.clipRect( + parent.paddingLeft, top, + parent.width - parent.paddingRight, bottom + ) + } else { + top = 0 + bottom = parent.height + } + val childCount = parent.childCount + for (i in 0 until childCount - 1) { + val child = parent.getChildAt(i) + parent.layoutManager!!.getDecoratedBoundsWithMargins(child, mBounds) + val right = mBounds.right + child.translationX.roundToInt() + val left = right - mDivider!!.intrinsicWidth + mDivider!!.setBounds(left, top, right, bottom) + mDivider!!.draw(canvas) + } + canvas.restore() + } + + override fun getItemOffsets( + outRect: Rect, view: View, parent: RecyclerView, + state: RecyclerView.State + ) { + if (mDivider == null) { + outRect[0, 0, 0] = 0 + return + } + + if (mOrientation == VERTICAL) { + outRect[0, 0, 0] = mDivider!!.intrinsicHeight + } else { + val childAdapterPosition = parent.getChildAdapterPosition(view) + val lastCount = parent.adapter!!.itemCount - 1 + //如果不是最后一条 正常赋值 如果是最后一条 赋值为0 + if (childAdapterPosition != lastCount) { + outRect[0, 0, mDivider!!.intrinsicWidth] = 0 + } else { + outRect[0, 0, 0] = 0 + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt new file mode 100755 index 000000000..185148d0a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/DragSelectTouchHelper.kt @@ -0,0 +1,1001 @@ +/* + * Copyright 2020 Mupceet + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.recycler + +import android.content.res.Resources +import android.text.TextUtils +import android.util.DisplayMetrics +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener +import io.legado.app.BuildConfig +import io.legado.app.ui.widget.recycler.DragSelectTouchHelper.AdvanceCallback.Mode +import io.legado.app.utils.DebugLog +import java.util.Locale +import kotlin.math.max +import kotlin.math.min + +/** + * @author mupceet + * !autoChangeMode +-------------------+ inactiveSelect() + * +------------------------------------> | | <--------------------+ + * | | Normal | | + * | activeDragSelect(position) | | activeSlideSelect() | + * | +------------------------------ | | ----------+ | + * | v +-------------------+ v | + * +-------------------+ autoChangeMode +-----------------------+ + * | Drag From Disable | ----------------------------------------------> | | + * +-------------------+ | | + * | | | | + * | | activeDragSelect(position) && allowDragInSlide | Slide | + * | | <---------------------------------------------- | | + * | Drag From Slide | | | + * | | | | + * | | ----------------------------------------------> | | + * +-------------------+ +-----------------------+ + */ +@Suppress("unused", "MemberVisibilityCanBePrivate", "DEPRECATION") +class DragSelectTouchHelper( + /** + * Developer callback which controls the behavior of DragSelectTouchHelper. + */ + private val mCallback: Callback, +) { + + companion object { + private const val TAG = "DSTH" + private const val MAX_HOTSPOT_RATIO = 0.5f + private val DEFAULT_EDGE_TYPE = EdgeType.INSIDE_EXTEND + private const val DEFAULT_HOTSPOT_RATIO = 0.2f + private const val DEFAULT_HOTSPOT_OFFSET = 0 + private const val DEFAULT_MAX_SCROLL_VELOCITY = 10 + private const val SELECT_STATE_NORMAL = 0x00 + private const val SELECT_STATE_SLIDE = 0x01 + private const val SELECT_STATE_DRAG_FROM_NORMAL = 0x10 + private const val SELECT_STATE_DRAG_FROM_SLIDE = 0x11 + } + + private val mDisplayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics + + /** + * Start of the slide area. + */ + private var mSlideAreaLeft = 0f + + /** + * End of the slide area. + */ + private var mSlideAreaRight = 0f + + /** + * The hotspot height by the ratio of RecyclerView. + */ + private var mHotspotHeightRatio = 0f + + /** + * The hotspot height. + */ + private var mHotspotHeight = 0f + + /** + * The hotspot offset. + */ + private var mHotspotOffset = 0f + + /** + * Whether should continue scrolling when move outside top hotspot region. + */ + private var mScrollAboveTopRegion = false + + /** + * Whether should continue scrolling when move outside bottom hotspot region. + */ + private var mScrollBelowBottomRegion = false + + /** + * The maximum velocity of auto scrolling. + */ + private var mMaximumVelocity = 0 + + /** + * Whether should auto enter slide mode after drag select finished. + */ + private var mShouldAutoChangeState = false + + /** + * Whether can drag selection in slide select mode. + */ + private var mIsAllowDragInSlideState = false + private var mRecyclerView: RecyclerView? = null + + /** + * The coordinate of hotspot area. + */ + private var mTopRegionFrom = -1f + private var mTopRegionTo = -1f + private var mBottomRegionFrom = -1f + private var mBottomRegionTo = -1f + private val mOnLayoutChangeListener = + View.OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + if (oldLeft != left || oldRight != right || oldTop != top || oldBottom != bottom) { + if (v === mRecyclerView) { + Logger.i( + "onLayoutChange:new: " + + left + " " + top + " " + right + " " + bottom + ) + Logger.i( + "onLayoutChange:old: " + + oldLeft + " " + oldTop + " " + oldRight + " " + oldBottom + ) + init(bottom - top) + } + } + } + + /** + * The current mode of selection. + */ + private var mSelectState = SELECT_STATE_NORMAL + + /** + * Whether is in top hotspot area. + */ + private var mIsInTopHotspot = false + + /** + * Whether is in bottom hotspot area. + */ + private var mIsInBottomHotspot = false + + /** + * Indicates automatically scroll. + */ + private var mIsScrolling = false + + /** + * The actual speed of the current moment. + */ + private var mScrollDistance = 0 + + /** + * The reference coordinate for the action start, used to avoid reverse scrolling. + */ + private var mDownY = Float.MIN_VALUE + + /** + * The reference coordinates for the last action. + */ + private var mLastX = Float.MIN_VALUE + private var mLastY = Float.MIN_VALUE + + /** + * The selected items position. + */ + private var mStart = RecyclerView.NO_POSITION + private var mEnd = RecyclerView.NO_POSITION + private var mLastRealStart = RecyclerView.NO_POSITION + private var mLastRealEnd = RecyclerView.NO_POSITION + private var mSlideStateStartPosition = RecyclerView.NO_POSITION + private var mHaveCalledSelectStart = false + private val mScrollRunnable: Runnable by lazy { + object : Runnable { + override fun run() { + if (mIsScrolling) { + scrollBy(mScrollDistance) + ViewCompat.postOnAnimation(mRecyclerView!!, this) + } + } + } + } + private val mOnItemTouchListener: OnItemTouchListener by lazy { + object : OnItemTouchListener { + override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { + Logger.d( + "onInterceptTouchEvent: x:" + e.x + ",y:" + e.y + + ", " + MotionEvent.actionToString(e.action) + ) + val adapter = rv.adapter + if (adapter == null || adapter.itemCount == 0) { + return false + } + var intercept = false + val action = e.action + when (action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN -> { + mDownY = e.y + // call the selection start's callback before moving + if (mSelectState == SELECT_STATE_SLIDE && isInSlideArea(e)) { + mSlideStateStartPosition = getItemPosition(rv, e) + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + mCallback.onSelectStart(mSlideStateStartPosition) + mHaveCalledSelectStart = true + } + intercept = true + } + } + MotionEvent.ACTION_MOVE -> if (mSelectState == SELECT_STATE_DRAG_FROM_NORMAL + || mSelectState == SELECT_STATE_DRAG_FROM_SLIDE + ) { + Logger.i("onInterceptTouchEvent: drag mode move") + intercept = true + } + MotionEvent.ACTION_UP -> { + if (mSelectState == SELECT_STATE_DRAG_FROM_NORMAL + || mSelectState == SELECT_STATE_DRAG_FROM_SLIDE + ) { + intercept = true + } + // finger is lifted before moving + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + selectFinished(mSlideStateStartPosition) + mSlideStateStartPosition = RecyclerView.NO_POSITION + } + // selection has triggered + if (mStart != RecyclerView.NO_POSITION) { + selectFinished(mEnd) + } + } + MotionEvent.ACTION_CANCEL -> { + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + selectFinished(mSlideStateStartPosition) + mSlideStateStartPosition = RecyclerView.NO_POSITION + } + if (mStart != RecyclerView.NO_POSITION) { + selectFinished(mEnd) + } + } + else -> { + } + } + // Intercept only when the selection is triggered + Logger.d("intercept result: $intercept") + return intercept + } + + override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { + if (!isActivated) { + return + } + Logger.d( + "onTouchEvent: x:" + e.x + ",y:" + e.y + + ", " + MotionEvent.actionToString(e.action) + ) + val action = e.action + when (action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_MOVE -> { + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + selectFirstItem(mSlideStateStartPosition) + // selection is triggered + mSlideStateStartPosition = RecyclerView.NO_POSITION + Logger.i("onTouchEvent: after slide mode down") + } + processAutoScroll(e) + if (!mIsInTopHotspot && !mIsInBottomHotspot) { + updateSelectedRange(rv, e) + } + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { + if (mSlideStateStartPosition != RecyclerView.NO_POSITION) { + selectFirstItem(mSlideStateStartPosition) + // selection is triggered + mSlideStateStartPosition = RecyclerView.NO_POSITION + Logger.i("onTouchEvent: after slide mode down") + } + if (!mIsInTopHotspot && !mIsInBottomHotspot) { + updateSelectedRange(rv, e) + } + selectFinished(mEnd) + } + } + } + + override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + if (disallowIntercept) { + inactiveSelect() + } + } + } + } + + init { + setHotspotRatio(DEFAULT_HOTSPOT_RATIO) + setHotspotOffset(DEFAULT_HOTSPOT_OFFSET) + setMaximumVelocity(DEFAULT_MAX_SCROLL_VELOCITY) + setEdgeType(DEFAULT_EDGE_TYPE) + setAutoEnterSlideState(false) + setAllowDragInSlideState(false) + setSlideArea(0, 0) + } + + /** + * Attaches the DragSelectTouchHelper to the provided RecyclerView. If TouchHelper is already + * attached to a RecyclerView, it will first detach from the previous one. You can call this + * method with `null` to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * `null` if you want to remove DragSelectTouchHelper from the + * current RecyclerView. + */ + fun attachToRecyclerView(recyclerView: RecyclerView?) { + if (mRecyclerView === recyclerView) { + return // nothing to do + } + mRecyclerView?.removeOnItemTouchListener(mOnItemTouchListener) + mRecyclerView = recyclerView + mRecyclerView?.let { + it.addOnItemTouchListener(mOnItemTouchListener) + it.addOnLayoutChangeListener(mOnLayoutChangeListener) + } + } + + /** + * Activate the slide selection mode. + */ + fun activeSlideSelect() { + activeSelectInternal(RecyclerView.NO_POSITION) + } + + /** + * Activate the selection mode with selected item position. Normally called on long press. + * + * @param position Indicates the position of selected item. + */ + fun activeDragSelect(position: Int) { + activeSelectInternal(position) + } + + /** + * Exit the selection mode. + */ + fun inactiveSelect() { + if (isActivated) { + selectFinished(mEnd) + } else { + selectFinished(RecyclerView.NO_POSITION) + } + Logger.logSelectStateChange(mSelectState, SELECT_STATE_NORMAL) + mSelectState = SELECT_STATE_NORMAL + } + + /** + * To determine whether it is in the selection mode. + * + * @return true if is in the selection mode. + */ + val isActivated: Boolean + get() = mSelectState != SELECT_STATE_NORMAL + + /** + * Sets hotspot height by ratio of RecyclerView. + * + * @param ratio range (0, 0.5). + * @return The select helper, which may used to chain setter calls. + */ + fun setHotspotRatio(ratio: Float): DragSelectTouchHelper { + mHotspotHeightRatio = ratio + return this + } + + /** + * Sets hotspot height. + * + * @param hotspotHeight hotspot height which unit is dp. + * @return The select helper, which may used to chain setter calls. + */ + fun setHotspotHeight(hotspotHeight: Int): DragSelectTouchHelper { + mHotspotHeight = dp2px(hotspotHeight.toFloat()).toFloat() + return this + } + + /** + * Sets hotspot offset. It don't need to be set if no special requirement. + * + * @param hotspotOffset hotspot offset which unit is dp. + * @return The select helper, which may used to chain setter calls. + */ + fun setHotspotOffset(hotspotOffset: Int): DragSelectTouchHelper { + mHotspotOffset = dp2px(hotspotOffset.toFloat()).toFloat() + return this + } + + /** + * Sets the activation edge type, one of: + * + * * [EdgeType.INSIDE] for edges that respond to touches inside + * the bounds of the host view. If touch moves outside the bounds, scrolling + * will stop. + * * [EdgeType.INSIDE_EXTEND] for inside edges that continued to + * scroll when touch moves outside the bounds of the host view. + * + * + * @param type The type of edge to use. + * @return The select helper, which may used to chain setter calls. + */ + fun setEdgeType(type: EdgeType?): DragSelectTouchHelper { + when (type) { + EdgeType.INSIDE -> { + mScrollAboveTopRegion = false + mScrollBelowBottomRegion = false + } + EdgeType.INSIDE_EXTEND -> { + mScrollAboveTopRegion = true + mScrollBelowBottomRegion = true + } + else -> { + mScrollAboveTopRegion = true + mScrollBelowBottomRegion = true + } + } + return this + } + + /** + * Sets sliding area's start and end, has been considered RTL situation + * + * @param startDp The start of the sliding area + * @param endDp The end of the sliding area + * @return The select helper, which may used to chain setter calls. + */ + fun setSlideArea(startDp: Int, endDp: Int): DragSelectTouchHelper { + if (!isRtl) { + mSlideAreaLeft = dp2px(startDp.toFloat()).toFloat() + mSlideAreaRight = dp2px(endDp.toFloat()).toFloat() + } else { + val displayWidth = mDisplayMetrics.widthPixels + mSlideAreaLeft = displayWidth - dp2px(endDp.toFloat()).toFloat() + mSlideAreaRight = displayWidth - dp2px(startDp.toFloat()).toFloat() + } + return this + } + + /** + * Sets the maximum velocity for scrolling + * + * @param velocity maximum velocity + * @return The select helper, which may used to chain setter calls. + */ + fun setMaximumVelocity(velocity: Int): DragSelectTouchHelper { + mMaximumVelocity = (velocity * mDisplayMetrics.density + 0.5f).toInt() + return this + } + + /** + * Sets whether should auto enter slide mode after drag select finished. + * It's usefully for LinearLayout RecyclerView. + * + * @param autoEnterSlideState should auto enter slide mode + * @return The select helper, which may used to chain setter calls. + */ + fun setAutoEnterSlideState(autoEnterSlideState: Boolean): DragSelectTouchHelper { + mShouldAutoChangeState = autoEnterSlideState + return this + } + + /** + * Sets whether can drag selection in slide select mode. + * It's usefully for LinearLayout RecyclerView. + * + * @param allowDragInSlideState allow drag selection in slide select mode + * @return The select helper, which may used to chain setter calls. + */ + fun setAllowDragInSlideState(allowDragInSlideState: Boolean): DragSelectTouchHelper { + mIsAllowDragInSlideState = allowDragInSlideState + return this + } + + private fun init(rvHeight: Int) { + if (mHotspotOffset >= rvHeight * MAX_HOTSPOT_RATIO) { + mHotspotOffset = rvHeight * MAX_HOTSPOT_RATIO + } + // The height of hotspot area is not set, using (RV height x ratio) + if (mHotspotHeight <= 0) { + if (mHotspotHeightRatio <= 0 || mHotspotHeightRatio >= MAX_HOTSPOT_RATIO) { + mHotspotHeightRatio = DEFAULT_HOTSPOT_RATIO + } + mHotspotHeight = rvHeight * mHotspotHeightRatio + } else { + if (mHotspotHeight >= rvHeight * MAX_HOTSPOT_RATIO) { + mHotspotHeight = rvHeight * MAX_HOTSPOT_RATIO + } + } + mTopRegionFrom = mHotspotOffset + mTopRegionTo = mTopRegionFrom + mHotspotHeight + mBottomRegionTo = rvHeight - mHotspotOffset + mBottomRegionFrom = mBottomRegionTo - mHotspotHeight + if (mTopRegionTo > mBottomRegionFrom) { + mBottomRegionFrom = (rvHeight shr 1.toFloat().toInt()).toFloat() + mTopRegionTo = mBottomRegionFrom + } + Logger.d( + "Hotspot: [" + mTopRegionFrom + ", " + mTopRegionTo + "], [" + + mBottomRegionFrom + ", " + mBottomRegionTo + "]" + ) + } + + private fun activeSelectInternal(position: Int) { + // We should initialize the hotspot here, because its data may be delayed load + mRecyclerView?.let { + init(it.height) + } + if (position == RecyclerView.NO_POSITION) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE) + mSelectState = SELECT_STATE_SLIDE + } else { + if (!mHaveCalledSelectStart) { + mCallback.onSelectStart(position) + mHaveCalledSelectStart = true + } + if (mSelectState == SELECT_STATE_SLIDE) { + if (mIsAllowDragInSlideState && selectFirstItem(position)) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_DRAG_FROM_SLIDE) + mSelectState = SELECT_STATE_DRAG_FROM_SLIDE + } + } else if (mSelectState == SELECT_STATE_NORMAL) { + if (selectFirstItem(position)) { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_DRAG_FROM_NORMAL) + mSelectState = SELECT_STATE_DRAG_FROM_NORMAL + } + } else { + Logger.e("activeSelect in unexpected state: $mSelectState") + } + } + } + + private fun selectFirstItem(position: Int): Boolean { + val selectFirstItemSucceed = mCallback.onSelectChange(position, true) + // The drag select feature is only available if the first item is available for selection + if (selectFirstItemSucceed) { + mStart = position + mEnd = position + mLastRealStart = position + mLastRealEnd = position + } + return selectFirstItemSucceed + } + + private fun updateSelectedRange(rv: RecyclerView, e: MotionEvent) { + updateSelectedRange(rv, e.x, e.y) + } + + private fun updateSelectedRange(rv: RecyclerView, x: Float, y: Float) { + val position = getItemPosition(rv, x, y) + if (position != RecyclerView.NO_POSITION && mEnd != position) { + mEnd = position + notifySelectRangeChange() + } + } + + private fun notifySelectRangeChange() { + if (mStart == RecyclerView.NO_POSITION || mEnd == RecyclerView.NO_POSITION) { + return + } + val newStart: Int = min(mStart, mEnd) + val newEnd: Int = max(mStart, mEnd) + if (mLastRealStart == RecyclerView.NO_POSITION || mLastRealEnd == RecyclerView.NO_POSITION) { + if (newEnd - newStart == 1) { + notifySelectChange(newStart, newStart, true) + } else { + notifySelectChange(newStart, newEnd, true) + } + } else { + if (newStart > mLastRealStart) { + notifySelectChange(mLastRealStart, newStart - 1, false) + } else if (newStart < mLastRealStart) { + notifySelectChange(newStart, mLastRealStart - 1, true) + } + if (newEnd > mLastRealEnd) { + notifySelectChange(mLastRealEnd + 1, newEnd, true) + } else if (newEnd < mLastRealEnd) { + notifySelectChange(newEnd + 1, mLastRealEnd, false) + } + } + mLastRealStart = newStart + mLastRealEnd = newEnd + } + + private fun notifySelectChange(start: Int, end: Int, newState: Boolean) { + for (i in start..end) { + mCallback.onSelectChange(i, newState) + } + } + + private fun selectFinished(lastItem: Int) { + if (lastItem != RecyclerView.NO_POSITION) { + mCallback.onSelectEnd(lastItem) + } + mStart = RecyclerView.NO_POSITION + mEnd = RecyclerView.NO_POSITION + mLastRealStart = RecyclerView.NO_POSITION + mLastRealEnd = RecyclerView.NO_POSITION + mHaveCalledSelectStart = false + mIsInTopHotspot = false + mIsInBottomHotspot = false + stopAutoScroll() + when (mSelectState) { + SELECT_STATE_DRAG_FROM_NORMAL -> mSelectState = if (mShouldAutoChangeState) { + Logger.logSelectStateChange( + mSelectState, + SELECT_STATE_SLIDE + ) + SELECT_STATE_SLIDE + } else { + Logger.logSelectStateChange( + mSelectState, + SELECT_STATE_NORMAL + ) + SELECT_STATE_NORMAL + } + SELECT_STATE_DRAG_FROM_SLIDE -> { + Logger.logSelectStateChange(mSelectState, SELECT_STATE_SLIDE) + mSelectState = SELECT_STATE_SLIDE + } + else -> { + } + } + } + + /** + * Process motion event, according to the location to determine whether to scroll + */ + private fun processAutoScroll(e: MotionEvent) { + val y = e.y + if (y in mTopRegionFrom..mTopRegionTo && y < mDownY) { + mLastX = e.x + mLastY = e.y + val scrollDistanceFactor = (y - mTopRegionTo) / mHotspotHeight + mScrollDistance = (mMaximumVelocity * scrollDistanceFactor).toInt() + if (!mIsInTopHotspot) { + mIsInTopHotspot = true + startAutoScroll() + mDownY = mTopRegionTo + } + } else if (mScrollAboveTopRegion && y < mTopRegionFrom && mIsInTopHotspot) { + mLastX = e.x + mLastY = mTopRegionFrom + // Use the maximum speed + mScrollDistance = mMaximumVelocity * -1 + startAutoScroll() + } else if (y in mBottomRegionFrom..mBottomRegionTo && y > mDownY) { + mLastX = e.x + mLastY = e.y + val scrollDistanceFactor = (y - mBottomRegionFrom) / mHotspotHeight + mScrollDistance = (mMaximumVelocity * scrollDistanceFactor).toInt() + if (!mIsInBottomHotspot) { + mIsInBottomHotspot = true + startAutoScroll() + mDownY = mBottomRegionFrom + } + } else if (mScrollBelowBottomRegion && y > mBottomRegionTo && mIsInBottomHotspot) { + mLastX = e.x + mLastY = mBottomRegionTo + // Use the maximum speed + mScrollDistance = mMaximumVelocity + startAutoScroll() + } else { + mIsInTopHotspot = false + mIsInBottomHotspot = false + mLastX = Float.MIN_VALUE + mLastY = Float.MIN_VALUE + stopAutoScroll() + } + } + + private fun startAutoScroll() { + if (!mIsScrolling) { + mIsScrolling = true + mRecyclerView!!.removeCallbacks(mScrollRunnable) + ViewCompat.postOnAnimation(mRecyclerView!!, mScrollRunnable) + } + } + + private fun stopAutoScroll() { + if (mIsScrolling) { + mIsScrolling = false + mRecyclerView?.removeCallbacks(mScrollRunnable) + } + } + + private fun scrollBy(distance: Int) { + val scrollDistance: Int = + if (distance > 0) { + min(distance, mMaximumVelocity) + } else { + max(distance, -mMaximumVelocity) + } + mRecyclerView!!.scrollBy(0, scrollDistance) + if (mLastX != Float.MIN_VALUE && mLastY != Float.MIN_VALUE) { + updateSelectedRange(mRecyclerView!!, mLastX, mLastY) + } + } + + private fun dp2px(dpVal: Float): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dpVal, mDisplayMetrics + ).toInt() + } + + private val isRtl: Boolean + get() = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) + == View.LAYOUT_DIRECTION_RTL) + + private fun isInSlideArea(e: MotionEvent): Boolean { + val x = e.x + return x > mSlideAreaLeft && x < mSlideAreaRight + } + + private fun getItemPosition(rv: RecyclerView, e: MotionEvent): Int { + return getItemPosition(rv, e.x, e.y) + } + + private fun getItemPosition(rv: RecyclerView, x: Float, y: Float): Int { + val v = rv.findChildViewUnder(x, y) + if (v == null) { + val layoutManager = rv.layoutManager + if (layoutManager is GridLayoutManager) { + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + val lastItemPosition = layoutManager.getItemCount() - 1 + if (lastItemPosition == lastVisibleItemPosition) { + return lastItemPosition + } + } + return RecyclerView.NO_POSITION + } + return rv.getChildAdapterPosition(v) + } + + /** + * Edge type that specifies an activation area starting at the view bounds and extending inward. + */ + enum class EdgeType { + /** + * After activation begins, moving outside the view bounds will stop scrolling. + */ + INSIDE, + + /** + * After activation begins, moving outside the view bounds will continue scrolling. + */ + INSIDE_EXTEND + } + + /** + * This class is the contract between DragSelectTouchHelper and your application. It lets you + * update adapter when selection start/end and state changed. + */ + abstract class Callback { + /** + * Called when changing item state. + * + * @param position this item want to change the state to new state. + * @param isSelected true if the position should be selected, false otherwise. + * @return Whether to set the new state successfully. + */ + abstract fun onSelectChange(position: Int, isSelected: Boolean): Boolean + + /** + * Called when selection start. + * + * @param start the first selected item. + */ + open fun onSelectStart(start: Int) {} + + /** + * Called when selection end. + * + * @param end the last selected item. + */ + open fun onSelectEnd(end: Int) {} + } + + /** + * An advance Callback which provide 4 useful selection modes [Mode]. + * + * + * Note: Since the state of item may be repeatedly set, in order to improve efficiency, + * please process it in the Adapter + */ + abstract class AdvanceCallback : Callback { + private var mMode: Mode? = null + private var mOriginalSelection: MutableSet = mutableSetOf() + private var mFirstWasSelected = false + + /** + * Creates a SimpleCallback with default [Mode.SelectAndReverse]# mode. + * + * @see Mode + */ + constructor() { + setMode(Mode.SelectAndReverse) + } + + /** + * Creates a SimpleCallback with select mode. + * + * @param mode the initial select mode + * @see Mode + */ + constructor(mode: Mode?) { + setMode(mode) + } + + /** + * Sets the select mode. + * + * @param mode The type of select mode. + * @see Mode + */ + fun setMode(mode: Mode?) { + mMode = mode + } + + override fun onSelectStart(start: Int) { + mOriginalSelection.clear() + val selected = currentSelectedId() + if (selected != null) { + mOriginalSelection.addAll(selected) + } + mFirstWasSelected = mOriginalSelection.contains(getItemId(start)) + } + + override fun onSelectEnd(end: Int) { + mOriginalSelection.clear() + } + + override fun onSelectChange(position: Int, isSelected: Boolean): Boolean { + return when (mMode) { + Mode.SelectAndKeep -> { + updateSelectState(position, true) + } + Mode.SelectAndReverse -> { + updateSelectState(position, isSelected) + } + Mode.SelectAndUndo -> { + if (isSelected) { + updateSelectState(position, true) + } else { + updateSelectState( + position, + mOriginalSelection.contains(getItemId(position)) + ) + } + } + Mode.ToggleAndKeep -> { + updateSelectState(position, !mFirstWasSelected) + } + Mode.ToggleAndReverse -> { + if (isSelected) { + updateSelectState(position, !mFirstWasSelected) + } else { + updateSelectState(position, mFirstWasSelected) + } + } + Mode.ToggleAndUndo -> { + if (isSelected) { + updateSelectState(position, !mFirstWasSelected) + } else { + updateSelectState( + position, + mOriginalSelection.contains(getItemId(position)) + ) + } + } + else -> // SelectAndReverse Mode + updateSelectState(position, isSelected) + } + } + + /** + * Get the currently selected items when selecting first item. + * + * @return the currently selected item's id set. + */ + abstract fun currentSelectedId(): Set? + + /** + * Get the ID of the item. + * + * @param position item position to be judged. + * @return item's identity. + */ + abstract fun getItemId(position: Int): T + + /** + * Update the selection status of the position. + * + * @param position the position who's selection state changed. + * @param isSelected true if the position should be selected, false otherwise. + * @return Whether to set the state successfully. + */ + abstract fun updateSelectState(position: Int, isSelected: Boolean): Boolean + + /** + * Different existing selection modes + */ + enum class Mode { + /** + * Selects the first item and applies the same state to each item you go by + * and keep the state on move back + */ + SelectAndKeep, + + /** + * Selects the first item and applies the same state to each item you go by + * and applies inverted state on move back + */ + SelectAndReverse, + + /** + * Selects the first item and applies the same state to each item you go by + * and reverts to the original state on move back + */ + SelectAndUndo, + + /** + * Toggles the first item and applies the same state to each item you go by + * and keep the state on move back + */ + ToggleAndKeep, + + /** + * Toggles the first item and applies the same state to each item you go by + * and applies inverted state on move back + */ + ToggleAndReverse, + + /** + * Toggles the first item and applies the same state to each item you go by + * and reverts to the original state on move back + */ + ToggleAndUndo + } + } + + private object Logger { + private val DEBUG = BuildConfig.DEBUG + fun d(msg: String) { + DebugLog.d(javaClass.name, msg) + } + + fun e(msg: String) { + DebugLog.e(javaClass.name, msg) + } + + fun i(msg: String) { + DebugLog.i(javaClass.name, msg) + } + + fun logSelectStateChange(before: Int, after: Int) { + i("Select state changed: " + stateName(before) + " --> " + stateName(after)) + } + + private fun stateName(state: Int): String { + return when (state) { + SELECT_STATE_NORMAL -> "NormalState" + SELECT_STATE_SLIDE -> "SlideState" + SELECT_STATE_DRAG_FROM_NORMAL -> "DragFromNormal" + SELECT_STATE_DRAG_FROM_SLIDE -> "DragFromSlide" + else -> "Unknown" + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/HeaderAdapterDataObserver.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/HeaderAdapterDataObserver.kt new file mode 100755 index 000000000..b112fa07f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/HeaderAdapterDataObserver.kt @@ -0,0 +1,34 @@ +package io.legado.app.ui.widget.recycler + +import androidx.recyclerview.widget.RecyclerView + +@Suppress("unused") +internal class HeaderAdapterDataObserver( + private var adapterDataObserver: RecyclerView.AdapterDataObserver, + private var headerCount: Int +) : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + adapterDataObserver.onChanged() + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { + adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount) + } + + override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { + adapterDataObserver.onItemRangeChanged(positionStart + headerCount, itemCount, payload) + } + + // 当第n个数据被获取,更新第n+1个position + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + adapterDataObserver.onItemRangeInserted(positionStart + headerCount, itemCount) + } + + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + adapterDataObserver.onItemRangeRemoved(positionStart + headerCount, itemCount) + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + super.onItemRangeMoved(fromPosition + headerCount, toPosition + headerCount, itemCount) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt new file mode 100755 index 000000000..be45df2cf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/ItemTouchCallback.kt @@ -0,0 +1,148 @@ +package io.legado.app.ui.widget.recycler + + +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +/** + * Created by GKF on 2018/3/16. + */ +@Suppress("MemberVisibilityCanBePrivate") +class ItemTouchCallback(private val callback: Callback) : ItemTouchHelper.Callback() { + + private var swipeRefreshLayout: SwipeRefreshLayout? = null + + /** + * 是否可以拖拽 + */ + var isCanDrag = false + + /** + * 是否可以被滑动 + */ + var isCanSwipe = false + + /** + * 当Item被长按的时候是否可以被拖拽 + */ + override fun isLongPressDragEnabled(): Boolean { + return isCanDrag + } + + /** + * Item是否可以被滑动(H:左右滑动,V:上下滑动) + */ + override fun isItemViewSwipeEnabled(): Boolean { + return isCanSwipe + } + + /** + * 当用户拖拽或者滑动Item的时候需要我们告诉系统滑动或者拖拽的方向 + */ + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + val layoutManager = recyclerView.layoutManager + if (layoutManager is GridLayoutManager) {// GridLayoutManager + // flag如果值是0,相当于这个功能被关闭 + val dragFlag = + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN + val swipeFlag = 0 + // create make + return makeMovementFlags(dragFlag, swipeFlag) + } else if (layoutManager is LinearLayoutManager) {// linearLayoutManager + val linearLayoutManager = layoutManager as LinearLayoutManager? + val orientation = linearLayoutManager!!.orientation + + var dragFlag = 0 + var swipeFlag = 0 + + // 为了方便理解,相当于分为横着的ListView和竖着的ListView + if (orientation == LinearLayoutManager.HORIZONTAL) {// 如果是横向的布局 + swipeFlag = ItemTouchHelper.UP or ItemTouchHelper.DOWN + dragFlag = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + } else if (orientation == LinearLayoutManager.VERTICAL) {// 如果是竖向的布局,相当于ListView + dragFlag = ItemTouchHelper.UP or ItemTouchHelper.DOWN + swipeFlag = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + } + return makeMovementFlags(dragFlag, swipeFlag) + } + return 0 + } + + /** + * 当Item被拖拽的时候被回调 + * + * @param recyclerView recyclerView + * @param srcViewHolder 拖拽的ViewHolder + * @param targetViewHolder 目的地的viewHolder + */ + override fun onMove( + recyclerView: RecyclerView, + srcViewHolder: RecyclerView.ViewHolder, + targetViewHolder: RecyclerView.ViewHolder + ): Boolean { + val fromPosition: Int = srcViewHolder.bindingAdapterPosition + val toPosition: Int = targetViewHolder.bindingAdapterPosition + if (fromPosition < toPosition) { + for (i in fromPosition until toPosition) { + callback.swap(i, i + 1) + } + } else { + for (i in fromPosition downTo toPosition + 1) { + callback.swap(i, i - 1) + } + } + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + callback.onSwiped(viewHolder.bindingAdapterPosition) + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + val swiping = actionState == ItemTouchHelper.ACTION_STATE_DRAG + swipeRefreshLayout?.isEnabled = !swiping + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + callback.onClearView(recyclerView, viewHolder) + } + + interface Callback { + + /** + * 当某个Item被滑动删除的时候 + * + * @param adapterPosition item的position + */ + fun onSwiped(adapterPosition: Int) { + + } + + /** + * 当两个Item位置互换的时候被回调 + * + * @param srcPosition 拖拽的item的position + * @param targetPosition 目的地的Item的position + * @return 开发者处理了操作应该返回true,开发者没有处理就返回false + */ + fun swap(srcPosition: Int, targetPosition: Int): Boolean { + return true + } + + /** + * 手指松开 + */ + fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + + } + + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt new file mode 100755 index 000000000..5ad794cd8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt @@ -0,0 +1,107 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.ColorRes +import io.legado.app.R +import io.legado.app.databinding.ViewLoadMoreBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.invisible +import io.legado.app.utils.visible + +@Suppress("unused") +class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + private val binding = ViewLoadMoreBinding.inflate(LayoutInflater.from(context), this) + private var errorMsg = "" + + private var onClickListener: OnClickListener? = null + + var isLoading = false + private set + + var hasMore = true + private set + + init { + super.setOnClickListener { + if (!showErrorDialog(it)) { + onClickListener?.onClick(it) + } + } + } + + override fun setOnClickListener(l: OnClickListener?) { + this.onClickListener = l + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + layoutParams.width = LayoutParams.MATCH_PARENT + } + + fun startLoad() { + isLoading = true + binding.tvText.invisible() + binding.rotateLoading.visible() + } + + fun stopLoad() { + isLoading = false + binding.rotateLoading.inVisible() + } + + fun hasMore() { + errorMsg = "" + hasMore = true + startLoad() + } + + fun noMore(msg: String? = null) { + stopLoad() + errorMsg = "" + hasMore = false + if (msg != null) { + binding.tvText.text = msg + } else { + binding.tvText.setText(R.string.bottom_line) + } + binding.tvText.visible() + } + + fun error(msg: String?, text: String = "") { + stopLoad() + hasMore = false + errorMsg = msg ?: "" + binding.tvText.text = + text.ifEmpty { context.getString(R.string.error_load_msg, "点击查看详情") } + binding.tvText.visible() + } + + fun setLoadingColor(@ColorRes color: Int) { + binding.rotateLoading.loadingColor = context.getCompatColor(color) + } + + fun setLoadingTextColor(@ColorRes color: Int) { + binding.tvText.setTextColor(context.getCompatColor(color)) + } + + private fun showErrorDialog(view: View): Boolean { + if (errorMsg.isBlank()) { + return false + } + context.alert(R.string.error) { + setMessage(errorMsg) + if (onClickListener != null) { + neutralButton(R.string.retry) { + onClickListener?.onClick(view) + } + } + } + return true + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/NoChildScrollLinearLayoutManager.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/NoChildScrollLinearLayoutManager.kt new file mode 100755 index 000000000..f57ce828d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/NoChildScrollLinearLayoutManager.kt @@ -0,0 +1,25 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class NoChildScrollLinearLayoutManager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : LinearLayoutManager(context, attrs, defStyleAttr, defStyleRes) { + + override fun onRequestChildFocus( + parent: RecyclerView, + state: RecyclerView.State, + child: View, + focused: View? + ): Boolean { + return true + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt new file mode 100755 index 000000000..1dc4d7b5f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/RecyclerViewAtPager2.kt @@ -0,0 +1,48 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs + +class RecyclerViewAtPager2 : RecyclerView { + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + private var startX = 0 + private var startY = 0 + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + startX = ev.x.toInt() + startY = ev.y.toInt() + parent.requestDisallowInterceptTouchEvent(true) + } + MotionEvent.ACTION_MOVE -> { + val endX = ev.x.toInt() + val endY = ev.y.toInt() + val disX = abs(endX - startX) + val disY = abs(endY - startY) + if (disX > disY) { + if (disX > 50) { + parent.requestDisallowInterceptTouchEvent(false) + } + } else { + parent.requestDisallowInterceptTouchEvent(true) + } + } + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false) + } + return super.dispatchTouchEvent(ev) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt new file mode 100755 index 000000000..f0236bbea --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/UpLinearLayoutManager.kt @@ -0,0 +1,46 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class UpLinearLayoutManager(val context: Context) : LinearLayoutManager(context) { + + fun smoothScrollToPosition(position: Int) { + smoothScrollToPosition(position, 0) + } + + fun smoothScrollToPosition(position: Int, offset: Int) { + val scroller = UpLinearSmoothScroller(context) + scroller.targetPosition = position + scroller.offset = offset + startSmoothScroll(scroller) + } + + class UpLinearSmoothScroller(context: Context?) : LinearSmoothScroller(context) { + var offset = 0 + + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun getHorizontalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun calculateDtToFit( + viewStart: Int, + viewEnd: Int, + boxStart: Int, + boxEnd: Int, + snapPreference: Int + ): Int { + if (snapPreference == SNAP_TO_START) { + return boxStart - viewStart + offset + } + throw IllegalArgumentException("snap preference should be SNAP_TO_START") + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/VerticalDivider.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/VerticalDivider.kt new file mode 100755 index 000000000..2ea450d4c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/VerticalDivider.kt @@ -0,0 +1,16 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import io.legado.app.R + +class VerticalDivider(context: Context) : DividerItemDecoration(context, VERTICAL) { + + init { + ContextCompat.getDrawable(context, R.drawable.ic_divider)?.let { + this.setDrawable(it) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/ViewPager2Container.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/ViewPager2Container.kt new file mode 100755 index 000000000..448e08286 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/ViewPager2Container.kt @@ -0,0 +1,122 @@ +package io.legado.app.ui.widget.recycler + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.RelativeLayout +import androidx.viewpager2.widget.ViewPager2 +import kotlin.math.abs + +@Suppress("unused") +class ViewPager2Container @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + private var mViewPager2: ViewPager2? = null + private var disallowParentInterceptDownEvent = true + private var startX = 0 + private var startY = 0 + + override fun onFinishInflate() { + super.onFinishInflate() + for (i in 0 until childCount) { + val childView = getChildAt(i) + if (childView is ViewPager2) { + mViewPager2 = childView + break + } + } + if (mViewPager2 == null) { + throw IllegalStateException("The root child of ViewPager2Container must contains a ViewPager2") + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + val doNotNeedIntercept = (!mViewPager2!!.isUserInputEnabled + || (mViewPager2?.adapter != null + && mViewPager2?.adapter!!.itemCount <= 1)) + if (doNotNeedIntercept) { + return super.onInterceptTouchEvent(ev) + } + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + startX = ev.x.toInt() + startY = ev.y.toInt() + parent.requestDisallowInterceptTouchEvent(!disallowParentInterceptDownEvent) + } + MotionEvent.ACTION_MOVE -> { + val endX = ev.x.toInt() + val endY = ev.y.toInt() + val disX = abs(endX - startX) + val disY = abs(endY - startY) + if (mViewPager2!!.orientation == ViewPager2.ORIENTATION_VERTICAL) { + onVerticalActionMove(endY, disX, disY) + } else if (mViewPager2!!.orientation == ViewPager2.ORIENTATION_HORIZONTAL) { + onHorizontalActionMove(endX, disX, disY) + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent( + false + ) + } + return super.onInterceptTouchEvent(ev) + } + + private fun onHorizontalActionMove(endX: Int, disX: Int, disY: Int) { + if (mViewPager2?.adapter == null) { + return + } + if (disX > disY) { + val currentItem = mViewPager2?.currentItem + val itemCount = mViewPager2?.adapter!!.itemCount + if (currentItem == 0 && endX - startX > 0) { + parent.requestDisallowInterceptTouchEvent(false) + } else { + parent.requestDisallowInterceptTouchEvent( + currentItem != itemCount - 1 + || endX - startX >= 0 + ) + } + } else if (disY > disX) { + parent.requestDisallowInterceptTouchEvent(false) + } + } + + private fun onVerticalActionMove(endY: Int, disX: Int, disY: Int) { + if (mViewPager2?.adapter == null) { + return + } + val currentItem = mViewPager2?.currentItem + val itemCount = mViewPager2?.adapter!!.itemCount + if (disY > disX) { + if (currentItem == 0 && endY - startY > 0) { + parent.requestDisallowInterceptTouchEvent(false) + } else { + parent.requestDisallowInterceptTouchEvent( + currentItem != itemCount - 1 + || endY - startY >= 0 + ) + } + } else if (disX > disY) { + parent.requestDisallowInterceptTouchEvent(false) + } + } + + /** + * 设置是否允许在当前View的{@link MotionEvent#ACTION_DOWN}事件中禁止父View对事件的拦截,该方法 + * 用于解决CoordinatorLayout+CollapsingToolbarLayout在嵌套ViewPager2Container时引起的滑动冲突问题。 + * + * 设置是否允许在ViewPager2Container的{@link MotionEvent#ACTION_DOWN}事件中禁止父View对事件的拦截,该方法 + * 用于解决CoordinatorLayout+CollapsingToolbarLayout在嵌套ViewPager2Container时引起的滑动冲突问题。 + * + * @param disallowParentInterceptDownEvent 是否允许ViewPager2Container在{@link MotionEvent#ACTION_DOWN}事件中禁止父View拦截事件,默认值为false + * true 不允许ViewPager2Container在{@link MotionEvent#ACTION_DOWN}时间中禁止父View的时间拦截, + * 设置disallowIntercept为true可以解决CoordinatorLayout+CollapsingToolbarLayout的滑动冲突 + * false 允许ViewPager2Container在{@link MotionEvent#ACTION_DOWN}时间中禁止父View的时间拦截, + */ + fun disallowParentInterceptDownEvent(disallowParentInterceptDownEvent: Boolean) { + this.disallowParentInterceptDownEvent = disallowParentInterceptDownEvent + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt new file mode 100755 index 000000000..d75f4f645 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollRecyclerView.kt @@ -0,0 +1,175 @@ +package io.legado.app.ui.widget.recycler.scroller + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.RelativeLayout +import androidx.annotation.ColorInt +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class FastScrollRecyclerView : RecyclerView { + + private lateinit var mFastScroller: FastScroller + + constructor(context: Context) : super(context) { + layout(context, null) + layoutParams = + LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } + + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet, + defStyleAttr: Int = 0 + ) : super(context, attrs, defStyleAttr) { + layout(context, attrs) + } + + private fun layout(context: Context, attrs: AttributeSet?) { + mFastScroller = FastScroller(context, attrs) + mFastScroller.id = R.id.fast_scroller + } + + override fun setAdapter(adapter: Adapter<*>?) { + super.setAdapter(adapter) + if (adapter is FastScroller.SectionIndexer) { + setSectionIndexer(adapter as FastScroller.SectionIndexer?) + } else if (adapter == null) { + setSectionIndexer(null) + } + } + + + override fun setVisibility(visibility: Int) { + super.setVisibility(visibility) + mFastScroller.visibility = visibility + } + + + /** + * Set the [FastScroller.SectionIndexer] for the [FastScroller]. + * + * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller + */ + fun setSectionIndexer(sectionIndexer: FastScroller.SectionIndexer?) { + mFastScroller.setSectionIndexer(sectionIndexer) + } + + + /** + * Set the enabled state of fast scrolling. + * + * @param enabled True to enable fast scrolling, false otherwise + */ + fun setFastScrollEnabled(enabled: Boolean) { + mFastScroller.isEnabled = enabled + } + + + /** + * Hide the scrollbar when not scrolling. + * + * @param hideScrollbar True to hide the scrollbar, false to show + */ + fun setHideScrollbar(hideScrollbar: Boolean) { + mFastScroller.setFadeScrollbar(hideScrollbar) + } + + /** + * Display a scroll track while scrolling. + * + * @param visible True to show scroll track, false to hide + */ + fun setTrackVisible(visible: Boolean) { + mFastScroller.setTrackVisible(visible) + } + + /** + * Set the color of the scroll track. + * + * @param color The color for the scroll track + */ + fun setTrackColor(@ColorInt color: Int) { + mFastScroller.setTrackColor(color) + } + + + /** + * Set the color for the scroll handle. + * + * @param color The color for the scroll handle + */ + fun setHandleColor(@ColorInt color: Int) { + mFastScroller.setHandleColor(color) + } + + + /** + * Show the section bubble while scrolling. + * + * @param visible True to show the bubble, false to hide + */ + fun setBubbleVisible(visible: Boolean) { + mFastScroller.setBubbleVisible(visible) + } + + + /** + * Set the background color of the index bubble. + * + * @param color The background color for the index bubble + */ + fun setBubbleColor(@ColorInt color: Int) { + mFastScroller.setBubbleColor(color) + } + + + /** + * Set the text color of the index bubble. + * + * @param color The text color for the index bubble + */ + fun setBubbleTextColor(@ColorInt color: Int) { + mFastScroller.setBubbleTextColor(color) + } + + + /** + * Set the fast scroll state change listener. + * + * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events + */ + fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) { + mFastScroller.setFastScrollStateChangeListener(fastScrollStateChangeListener) + } + + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mFastScroller.attachRecyclerView(this) + var parent = parent + while (parent != null) { + when (parent) { + is ConstraintLayout, is CoordinatorLayout, is FrameLayout, is RelativeLayout -> break + else -> parent = parent.parent + } + } + if (parent is ViewGroup && parent.indexOfChild(mFastScroller) == -1) { + parent.addView(mFastScroller) + mFastScroller.setLayoutParams(parent) + } + } + + + override fun onDetachedFromWindow() { + mFastScroller.detachRecyclerView() + super.onDetachedFromWindow() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt new file mode 100755 index 000000000..55afa8370 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScrollStateChangeListener.kt @@ -0,0 +1,14 @@ +package io.legado.app.ui.widget.recycler.scroller + +interface FastScrollStateChangeListener { + + /** + * Called when fast scrolling begins + */ + fun onFastScrollStart(fastScroller: FastScroller) + + /** + * Called when fast scrolling ends + */ + fun onFastScrollStop(fastScroller: FastScroller) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt new file mode 100755 index 000000000..897a28103 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt @@ -0,0 +1,549 @@ +package io.legado.app.ui.widget.recycler.scroller + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewPropertyAnimator +import android.widget.* +import androidx.annotation.ColorInt +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.GravityCompat +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.getCompatColor +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + + +@Suppress("SameParameterValue") +class FastScroller : LinearLayout { + @ColorInt + private var mBubbleColor: Int = 0 + + @ColorInt + private var mHandleColor: Int = 0 + private var mBubbleHeight: Int = 0 + private var mHandleHeight: Int = 0 + private var mViewHeight: Int = 0 + private var mFadeScrollbar: Boolean = false + private var mShowBubble: Boolean = false + private var mSectionIndexer: SectionIndexer? = null + private var mScrollbarAnimator: ViewPropertyAnimator? = null + private var mBubbleAnimator: ViewPropertyAnimator? = null + private var mRecyclerView: RecyclerView? = null + private lateinit var mBubbleView: TextView + private lateinit var mHandleView: ImageView + private lateinit var mTrackView: ImageView + private lateinit var mScrollbar: View + private var mBubbleImage: Drawable? = null + private var mHandleImage: Drawable? = null + private var mTrackImage: Drawable? = null + private var mFastScrollStateChangeListener: FastScrollStateChangeListener? = null + private val mScrollbarHider = Runnable { this.hideScrollbar() } + + private val mScrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (!mHandleView.isSelected && isEnabled) { + setViewPositions(getScrollProportion(recyclerView)) + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (isEnabled) { + when (newState) { + RecyclerView.SCROLL_STATE_DRAGGING -> { + handler.removeCallbacks(mScrollbarHider) + cancelAnimation(mScrollbarAnimator) + if (!isViewVisible(mScrollbar)) { + showScrollbar() + } + } + RecyclerView.SCROLL_STATE_IDLE -> if (mFadeScrollbar && !mHandleView.isSelected) { + handler.postDelayed(mScrollbarHider, sScrollbarHideDelay.toLong()) + } + } + } + } + } + + constructor(context: Context) : super(context) { + layout(context, null) + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT) + } + + @JvmOverloads + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : super( + context, + attrs, + defStyleAttr + ) { + layout(context, attrs) + layoutParams = generateLayoutParams(attrs) + } + + override fun setLayoutParams(params: ViewGroup.LayoutParams) { + params.width = LayoutParams.WRAP_CONTENT + super.setLayoutParams(params) + } + + fun setLayoutParams(viewGroup: ViewGroup) { + @IdRes val recyclerViewId = mRecyclerView?.id ?: View.NO_ID + val marginTop = resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top) + val marginBottom = + resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom) + require(recyclerViewId != View.NO_ID) { "RecyclerView must have a view ID" } + when (viewGroup) { + is ConstraintLayout -> { + val constraintSet = ConstraintSet() + @IdRes val layoutId = id + constraintSet.clone(viewGroup) + constraintSet.connect( + layoutId, + ConstraintSet.TOP, + recyclerViewId, + ConstraintSet.TOP + ) + constraintSet.connect( + layoutId, + ConstraintSet.BOTTOM, + recyclerViewId, + ConstraintSet.BOTTOM + ) + constraintSet.connect( + layoutId, + ConstraintSet.END, + recyclerViewId, + ConstraintSet.END + ) + constraintSet.applyTo(viewGroup) + val layoutParams = layoutParams as ConstraintLayout.LayoutParams + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + is CoordinatorLayout -> { + val layoutParams = layoutParams as CoordinatorLayout.LayoutParams + layoutParams.anchorId = recyclerViewId + layoutParams.anchorGravity = GravityCompat.END + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + is FrameLayout -> { + val layoutParams = layoutParams as FrameLayout.LayoutParams + layoutParams.gravity = GravityCompat.END + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + is RelativeLayout -> { + val layoutParams = layoutParams as RelativeLayout.LayoutParams + val endRule = RelativeLayout.ALIGN_END + layoutParams.addRule(RelativeLayout.ALIGN_TOP, recyclerViewId) + layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId) + layoutParams.addRule(endRule, recyclerViewId) + layoutParams.setMargins(0, marginTop, 0, marginBottom) + setLayoutParams(layoutParams) + } + else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout") + } + updateViewHeights() + } + + fun setSectionIndexer(sectionIndexer: SectionIndexer?) { + mSectionIndexer = sectionIndexer + } + + fun attachRecyclerView(recyclerView: RecyclerView) { + mRecyclerView = recyclerView + mRecyclerView!!.addOnScrollListener(mScrollListener) + post { + // set initial positions for bubble and handle + setViewPositions(getScrollProportion(mRecyclerView)) + } + } + + fun detachRecyclerView() { + if (mRecyclerView != null) { + mRecyclerView!!.removeOnScrollListener(mScrollListener) + mRecyclerView = null + } + } + + /** + * Hide the scrollbar when not scrolling. + * + * @param fadeScrollbar True to hide the scrollbar, false to show + */ + fun setFadeScrollbar(fadeScrollbar: Boolean) { + mFadeScrollbar = fadeScrollbar + mScrollbar.visibility = if (fadeScrollbar) View.INVISIBLE else View.VISIBLE + } + + /** + * Show the section bubble while scrolling. + * + * @param visible True to show the bubble, false to hide + */ + fun setBubbleVisible(visible: Boolean) { + mShowBubble = visible + } + + /** + * Display a scroll track while scrolling. + * + * @param visible True to show scroll track, false to hide + */ + fun setTrackVisible(visible: Boolean) { + mTrackView.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + + /** + * Set the color of the scroll track. + * + * @param color The color for the scroll track + */ + fun setTrackColor(@ColorInt color: Int) { + if (mTrackImage == null) { + val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_track) + if (drawable != null) { + mTrackImage = DrawableCompat.wrap(drawable) + } + } + DrawableCompat.setTint(mTrackImage!!, color) + mTrackView.setImageDrawable(mTrackImage) + } + + /** + * Set the color for the scroll handle. + * + * @param color The color for the scroll handle + */ + fun setHandleColor(@ColorInt color: Int) { + mHandleColor = color + if (mHandleImage == null) { + val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_handle) + if (drawable != null) { + mHandleImage = DrawableCompat.wrap(drawable) + } + } + DrawableCompat.setTint(mHandleImage!!, mHandleColor) + mHandleView.setImageDrawable(mHandleImage) + } + + /** + * Set the background color of the index bubble. + * + * @param color The background color for the index bubble + */ + fun setBubbleColor(@ColorInt color: Int) { + mBubbleColor = color + if (mBubbleImage == null) { + val drawable = ContextCompat.getDrawable(context, R.drawable.fastscroll_bubble) + if (drawable != null) { + mBubbleImage = DrawableCompat.wrap(drawable) + } + } + DrawableCompat.setTint(mBubbleImage!!, mBubbleColor) + mBubbleView.background = mBubbleImage + } + + /** + * Set the text color of the index bubble. + * + * @param color The text color for the index bubble + */ + fun setBubbleTextColor(@ColorInt color: Int) { + mBubbleView.setTextColor(color) + } + + /** + * Set the fast scroll state change listener. + * + * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events + */ + fun setFastScrollStateChangeListener(fastScrollStateChangeListener: FastScrollStateChangeListener) { + mFastScrollStateChangeListener = fastScrollStateChangeListener + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + visibility = if (enabled) View.VISIBLE else View.INVISIBLE + } + + @Suppress("DEPRECATION") + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (event.x < mHandleView.x - ViewCompat.getPaddingStart(mHandleView)) { + return false + } + if (!mScrollbar.isVisible) { + return false + } + requestDisallowInterceptTouchEvent(true) + setHandleSelected(true) + handler.removeCallbacks(mScrollbarHider) + cancelAnimation(mScrollbarAnimator) + cancelAnimation(mBubbleAnimator) + if (mShowBubble && mSectionIndexer != null) { + showBubble() + } + if (mFastScrollStateChangeListener != null) { + mFastScrollStateChangeListener!!.onFastScrollStart(this) + } + val y = event.y + setViewPositions(y) + setRecyclerViewPosition(y) + return true + } + MotionEvent.ACTION_MOVE -> { + val y = event.y + setViewPositions(y) + setRecyclerViewPosition(y) + return true + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + requestDisallowInterceptTouchEvent(false) + setHandleSelected(false) + if (mFadeScrollbar) { + handler.postDelayed(mScrollbarHider, sScrollbarHideDelay.toLong()) + } + hideBubble() + if (mFastScrollStateChangeListener != null) { + mFastScrollStateChangeListener!!.onFastScrollStop(this) + } + return true + } + } + return super.onTouchEvent(event) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + mViewHeight = h + } + + private fun setRecyclerViewPosition(y: Float) { + mRecyclerView?.adapter?.let { adapter -> + val itemCount = adapter.itemCount + val proportion: Float = when { + mHandleView.y == 0f -> 0f + mHandleView.y + mHandleHeight >= mViewHeight - sTrackSnapRange -> 1f + else -> y / mViewHeight.toFloat() + } + var scrolledItemCount = (proportion * itemCount).roundToInt() + if (isLayoutReversed(mRecyclerView?.layoutManager)) { + scrolledItemCount = itemCount - scrolledItemCount + } + val targetPos = getValueInRange(0, itemCount - 1, scrolledItemCount) + mRecyclerView?.layoutManager?.scrollToPosition(targetPos) + mSectionIndexer?.let { sectionIndexer -> + if (mShowBubble) { + mBubbleView.text = sectionIndexer.getSectionText(targetPos) + } + } + } + } + + private fun getScrollProportion(recyclerView: RecyclerView?): Float { + recyclerView ?: return 0f + val verticalScrollOffset = recyclerView.computeVerticalScrollOffset() + val verticalScrollRange = recyclerView.computeVerticalScrollRange() + val rangeDiff = (verticalScrollRange - mViewHeight).toFloat() + val proportion = verticalScrollOffset.toFloat() / if (rangeDiff > 0) rangeDiff else 1f + return mViewHeight * proportion + } + + private fun getValueInRange(min: Int, max: Int, value: Int): Int { + val minimum = max(min, value) + return min(minimum, max) + } + + private fun setViewPositions(y: Float) { + mBubbleHeight = mBubbleView.height + mHandleHeight = mHandleView.height + val bubbleY = getValueInRange( + 0, + mViewHeight - mBubbleHeight - mHandleHeight / 2, + (y - mBubbleHeight).toInt() + ) + val handleY = + getValueInRange(0, mViewHeight - mHandleHeight, (y - mHandleHeight / 2).toInt()) + if (mShowBubble) { + mBubbleView.y = bubbleY.toFloat() + } + mHandleView.y = handleY.toFloat() + } + + private fun updateViewHeights() { + val measureSpec = + MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + mBubbleView.measure(measureSpec, measureSpec) + mBubbleHeight = mBubbleView.measuredHeight + mHandleView.measure(measureSpec, measureSpec) + mHandleHeight = mHandleView.measuredHeight + } + + private fun isLayoutReversed(layoutManager: RecyclerView.LayoutManager?): Boolean { + if (layoutManager is LinearLayoutManager) { + return layoutManager.reverseLayout + } else if (layoutManager is StaggeredGridLayoutManager) { + return layoutManager.reverseLayout + } + return false + } + + private fun isViewVisible(view: View?): Boolean { + return view != null && view.visibility == View.VISIBLE + } + + private fun cancelAnimation(animator: ViewPropertyAnimator?) { + animator?.cancel() + } + + private fun showBubble() { + if (!isViewVisible(mBubbleView)) { + mBubbleView.visibility = View.VISIBLE + mBubbleAnimator = mBubbleView.animate().alpha(1f) + .setDuration(sBubbleAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + + // adapter required for new alpha value to stick + }) + } + } + + private fun hideBubble() { + if (isViewVisible(mBubbleView)) { + mBubbleAnimator = mBubbleView.animate().alpha(0f) + .setDuration(sBubbleAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + mBubbleView.visibility = View.INVISIBLE + mBubbleAnimator = null + } + + override fun onAnimationCancel(animation: Animator) { + super.onAnimationCancel(animation) + mBubbleView.visibility = View.INVISIBLE + mBubbleAnimator = null + } + }) + } + } + + private fun showScrollbar() { + mRecyclerView?.let { mRecyclerView -> + if (mRecyclerView.computeVerticalScrollRange() - mViewHeight > 0) { + val transX = + resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end) + .toFloat() + mScrollbar.translationX = transX + mScrollbar.visibility = View.VISIBLE + mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f) + .setDuration(sScrollbarAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + + // adapter required for new alpha value to stick + }) + } + } + } + + private fun hideScrollbar() { + val transX = + resources.getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end).toFloat() + mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f) + .setDuration(sScrollbarAnimDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + mScrollbar.visibility = View.INVISIBLE + mScrollbarAnimator = null + } + + override fun onAnimationCancel(animation: Animator) { + super.onAnimationCancel(animation) + mScrollbar.visibility = View.INVISIBLE + mScrollbarAnimator = null + } + }) + } + + private fun setHandleSelected(selected: Boolean) { + mHandleView.isSelected = selected + DrawableCompat.setTint(mHandleImage!!, if (selected) mBubbleColor else mHandleColor) + } + + private fun layout(context: Context, attrs: AttributeSet?) { + View.inflate(context, R.layout.view_fastscroller, this) + clipChildren = false + orientation = HORIZONTAL + mBubbleView = findViewById(R.id.fastscroll_bubble) + mHandleView = findViewById(R.id.fastscroll_handle) + mTrackView = findViewById(R.id.fastscroll_track) + mScrollbar = findViewById(R.id.fastscroll_scrollbar) + @ColorInt var bubbleColor = ColorUtils.adjustAlpha(context.accentColor, 0.8f) + @ColorInt var handleColor = context.accentColor + @ColorInt var trackColor = context.getCompatColor(R.color.transparent30) + @ColorInt var textColor = + if (ColorUtils.isColorLight(bubbleColor)) Color.BLACK else Color.WHITE + var fadeScrollbar = true + var showBubble = false + var showTrack = true + if (attrs != null) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0) + try { + bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor) + handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor) + trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor) + textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor) + fadeScrollbar = + typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar) + showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble) + showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack) + } finally { + typedArray.recycle() + } + } + setTrackColor(trackColor) + setHandleColor(handleColor) + setBubbleColor(bubbleColor) + setBubbleTextColor(textColor) + setFadeScrollbar(fadeScrollbar) + setBubbleVisible(showBubble) + setTrackVisible(showTrack) + } + + interface SectionIndexer { + fun getSectionText(position: Int): String + } + + companion object { + private const val sBubbleAnimDuration = 100 + private const val sScrollbarAnimDuration = 300 + private const val sScrollbarHideDelay = 1000 + private const val sTrackSnapRange = 5 + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt b/app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt new file mode 100755 index 000000000..eaa159029 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/seekbar/SeekBarChangeListener.kt @@ -0,0 +1,19 @@ +package io.legado.app.ui.widget.seekbar + +import android.widget.SeekBar + +interface SeekBarChangeListener : SeekBar.OnSeekBarChangeListener { + + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt new file mode 100755 index 000000000..c9d05cc46 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBar.kt @@ -0,0 +1,346 @@ +package io.legado.app.ui.widget.seekbar + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.MotionEvent +import android.widget.ProgressBar +import androidx.appcompat.widget.AppCompatSeekBar +import androidx.core.view.ViewCompat +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +@Suppress("SameParameterValue") +class VerticalSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + AppCompatSeekBar(context, attrs) { + + private var mIsDragging: Boolean = false + private var mThumb: Drawable? = null + private var mMethodSetProgressFromUser: Method? = null + private var mRotationAngle = ROTATION_ANGLE_CW_90 + + var rotationAngle: Int + get() = mRotationAngle + set(angle) { + require(isValidRotationAngle(angle)) { "Invalid angle specified :$angle" } + + if (mRotationAngle == angle) { + return + } + + mRotationAngle = angle + + if (useViewRotation()) { + val wrapper = wrapper + wrapper?.applyViewRotation() + } else { + requestLayout() + } + } + + private val wrapper: VerticalSeekBarWrapper? + get() { + val parent = parent + + return if (parent is VerticalSeekBarWrapper) { + parent + } else { + null + } + } + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + @Suppress("DEPRECATION") + ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR) + + if (attrs != null) { + val a = context.obtainStyledAttributes(attrs, R.styleable.VerticalSeekBar) + val rotationAngle = a.getInteger(R.styleable.VerticalSeekBar_seekBarRotation, 0) + if (isValidRotationAngle(rotationAngle)) { + mRotationAngle = rotationAngle + } + a.recycle() + } + } + + override fun setThumb(thumb: Drawable?) { + mThumb = thumb + super.setThumb(thumb) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + return if (useViewRotation()) { + onTouchEventUseViewRotation(event) + } else { + onTouchEventTraditionalRotation(event) + } + } + + private fun onTouchEventTraditionalRotation(event: MotionEvent): Boolean { + if (!isEnabled) { + return false + } + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isPressed = true + onStartTrackingTouch() + trackTouchEvent(event) + attemptClaimDrag(true) + invalidate() + } + + MotionEvent.ACTION_MOVE -> if (mIsDragging) { + trackTouchEvent(event) + } + + MotionEvent.ACTION_UP -> { + if (mIsDragging) { + trackTouchEvent(event) + onStopTrackingTouch() + isPressed = false + } else { + // Touch up when we never crossed the touch slop threshold + // should + // be interpreted as a tap-seek to that location. + onStartTrackingTouch() + trackTouchEvent(event) + onStopTrackingTouch() + attemptClaimDrag(false) + } + // ProgressBar doesn't know to repaint the thumb drawable + // in its inactive state when the touch stops (because the + // value has not apparently changed) + invalidate() + } + + MotionEvent.ACTION_CANCEL -> { + if (mIsDragging) { + onStopTrackingTouch() + isPressed = false + } + invalidate() // see above explanation + } + } + return true + } + + private fun onTouchEventUseViewRotation(event: MotionEvent): Boolean { + val handled = super.onTouchEvent(event) + + if (handled) { + when (event.action) { + MotionEvent.ACTION_DOWN -> attemptClaimDrag(true) + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> attemptClaimDrag(false) + } + } + + return handled + } + + private fun trackTouchEvent(event: MotionEvent) { + val paddingLeft = super.getPaddingLeft() + val paddingRight = super.getPaddingRight() + val height = height + + val available = height - paddingLeft - paddingRight + val y = event.y.toInt() + + val scale: Float + var value = 0f + + when (mRotationAngle) { + ROTATION_ANGLE_CW_90 -> value = (y - paddingLeft).toFloat() + ROTATION_ANGLE_CW_270 -> value = (height - paddingLeft - y).toFloat() + } + + scale = if (value < 0 || available == 0) { + 0.0f + } else if (value > available) { + 1.0f + } else { + value / available.toFloat() + } + + val max = max + val progress = scale * max + + setProgressFromUser(progress.toInt(), true) + } + + /** + * Tries to claim the user's drag motion, and requests disallowing any + * ancestors from stealing events in the drag. + */ + private fun attemptClaimDrag(active: Boolean) { + val parent = parent + parent?.requestDisallowInterceptTouchEvent(active) + } + + /** + * This is called when the user has started touching this widget. + */ + private fun onStartTrackingTouch() { + mIsDragging = true + } + + /** + * This is called when the user either releases his touch or the touch is + * canceled. + */ + private fun onStopTrackingTouch() { + mIsDragging = false + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (isEnabled) { + val handled: Boolean + var direction = 0 + + when (keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> { + direction = if (mRotationAngle == ROTATION_ANGLE_CW_90) 1 else -1 + handled = true + } + + KeyEvent.KEYCODE_DPAD_UP -> { + direction = if (mRotationAngle == ROTATION_ANGLE_CW_270) 1 else -1 + handled = true + } + + KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT -> + // move view focus to previous/next view + return false + + else -> handled = false + } + + if (handled) { + val keyProgressIncrement = keyProgressIncrement + var progress = progress + + progress += direction * keyProgressIncrement + + if (progress in 0..max) { + setProgressFromUser(progress, true) + } + + return true + } + } + + return super.onKeyDown(keyCode, event) + } + + @Synchronized + override fun setProgress(progress: Int) { + super.setProgress(progress) + if (!useViewRotation()) { + refreshThumb() + } + } + + @Synchronized + private fun setProgressFromUser(progress: Int, fromUser: Boolean) { + if (mMethodSetProgressFromUser == null) { + try { + val m: Method = ProgressBar::class.java.getDeclaredMethod( + "setProgress", + Int::class.javaPrimitiveType, + Boolean::class.javaPrimitiveType + ) + m.isAccessible = true + mMethodSetProgressFromUser = m + } catch (_: NoSuchMethodException) { + } + + } + + if (mMethodSetProgressFromUser != null) { + try { + mMethodSetProgressFromUser!!.invoke(this, progress, fromUser) + } catch (_: IllegalArgumentException) { + } catch (_: IllegalAccessException) { + } catch (_: InvocationTargetException) { + } + + } else { + super.setProgress(progress) + } + refreshThumb() + } + + @Synchronized + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (useViewRotation()) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } else { + super.onMeasure(heightMeasureSpec, widthMeasureSpec) + + val lp = layoutParams + + if (isInEditMode && lp != null && lp.height >= 0) { + setMeasuredDimension(super.getMeasuredHeight(), lp.height) + } else { + setMeasuredDimension(super.getMeasuredHeight(), super.getMeasuredWidth()) + } + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + if (useViewRotation()) { + super.onSizeChanged(w, h, oldw, oldh) + } else { + super.onSizeChanged(h, w, oldh, oldw) + } + } + + @Synchronized + override fun onDraw(canvas: Canvas) { + if (!useViewRotation()) { + when (mRotationAngle) { + ROTATION_ANGLE_CW_90 -> { + canvas.rotate(90f) + canvas.translate(0f, (-super.getWidth()).toFloat()) + } + + ROTATION_ANGLE_CW_270 -> { + canvas.rotate(-90f) + canvas.translate((-super.getHeight()).toFloat(), 0f) + } + } + } + + super.onDraw(canvas) + } + + // refresh thumb position + private fun refreshThumb() { + onSizeChanged(super.getWidth(), super.getHeight(), 0, 0) + } + + /*package*/ + internal fun useViewRotation(): Boolean { + return !isInEditMode + } + + companion object { + const val ROTATION_ANGLE_CW_90 = 90 + const val ROTATION_ANGLE_CW_270 = 270 + + private fun isValidRotationAngle(angle: Int): Boolean { + return angle == ROTATION_ANGLE_CW_90 || angle == ROTATION_ANGLE_CW_270 + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt new file mode 100755 index 000000000..a22f3a568 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/seekbar/VerticalSeekBarWrapper.kt @@ -0,0 +1,184 @@ +package io.legado.app.ui.widget.seekbar + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout + +import androidx.core.view.ViewCompat +import kotlin.math.max + +class VerticalSeekBarWrapper @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + + private val childSeekBar: VerticalSeekBar? + get() { + val child = if (childCount > 0) getChildAt(0) else null + return if (child is VerticalSeekBar) child else null + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + if (useViewRotation()) { + onSizeChangedUseViewRotation(w, h, oldw, oldh) + } else { + onSizeChangedTraditionalRotation(w, h, oldw, oldh) + } + } + + @SuppressLint("RtlHardcoded") + private fun onSizeChangedTraditionalRotation(w: Int, h: Int, oldw: Int, oldh: Int) { + val seekBar = childSeekBar + + if (seekBar != null) { + val hPadding = paddingLeft + paddingRight + val vPadding = paddingTop + paddingBottom + val lp = seekBar.layoutParams as LayoutParams + + lp.width = ViewGroup.LayoutParams.WRAP_CONTENT + lp.height = max(0, h - vPadding) + seekBar.layoutParams = lp + + seekBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + + val seekBarMeasuredWidth = seekBar.measuredWidth + seekBar.measure( + MeasureSpec.makeMeasureSpec( + max(0, w - hPadding), + MeasureSpec.AT_MOST + ), + MeasureSpec.makeMeasureSpec( + max(0, h - vPadding), + MeasureSpec.EXACTLY + ) + ) + + lp.gravity = Gravity.TOP or Gravity.LEFT + lp.leftMargin = (max(0, w - hPadding) - seekBarMeasuredWidth) / 2 + seekBar.layoutParams = lp + } + + super.onSizeChanged(w, h, oldw, oldh) + } + + private fun onSizeChangedUseViewRotation(w: Int, h: Int, oldw: Int, oldh: Int) { + val seekBar = childSeekBar + + if (seekBar != null) { + val hPadding = paddingLeft + paddingRight + val vPadding = paddingTop + paddingBottom + seekBar.measure( + MeasureSpec.makeMeasureSpec( + max(0, h - vPadding), + MeasureSpec.EXACTLY + ), + MeasureSpec.makeMeasureSpec( + max(0, w - hPadding), + MeasureSpec.AT_MOST + ) + ) + } + + applyViewRotation(w, h) + super.onSizeChanged(w, h, oldw, oldh) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val seekBar = childSeekBar + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + if (seekBar != null && widthMode != MeasureSpec.EXACTLY) { + val seekBarWidth: Int + val seekBarHeight: Int + val hPadding = paddingLeft + paddingRight + val vPadding = paddingTop + paddingBottom + val innerContentWidthMeasureSpec = + MeasureSpec.makeMeasureSpec(max(0, widthSize - hPadding), widthMode) + val innerContentHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(max(0, heightSize - vPadding), heightMode) + + if (useViewRotation()) { + seekBar.measure(innerContentHeightMeasureSpec, innerContentWidthMeasureSpec) + seekBarWidth = seekBar.measuredHeight + seekBarHeight = seekBar.measuredWidth + } else { + seekBar.measure(innerContentWidthMeasureSpec, innerContentHeightMeasureSpec) + seekBarWidth = seekBar.measuredWidth + seekBarHeight = seekBar.measuredHeight + } + + val measuredWidth = + View.resolveSizeAndState(seekBarWidth + hPadding, widthMeasureSpec, 0) + val measuredHeight = + View.resolveSizeAndState(seekBarHeight + vPadding, heightMeasureSpec, 0) + + setMeasuredDimension(measuredWidth, measuredHeight) + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + } + + /*package*/ + internal fun applyViewRotation() { + applyViewRotation(width, height) + } + + @Suppress("DEPRECATION") + private fun applyViewRotation(w: Int, h: Int) { + val seekBar = childSeekBar + + if (seekBar != null) { + val isLTR = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR + val rotationAngle = seekBar.rotationAngle + val seekBarMeasuredWidth = seekBar.measuredWidth + val seekBarMeasuredHeight = seekBar.measuredHeight + val hPadding = paddingLeft + paddingRight + val vPadding = paddingTop + paddingBottom + val hOffset = (max(0, w - hPadding) - seekBarMeasuredHeight) * 0.5f + val lp = seekBar.layoutParams + + lp.width = max(0, h - vPadding) + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT + + seekBar.layoutParams = lp + + seekBar.pivotX = (if (isLTR) 0 else max(0, h - vPadding)).toFloat() + seekBar.pivotY = 0f + + when (rotationAngle) { + VerticalSeekBar.ROTATION_ANGLE_CW_90 -> { + seekBar.rotation = 90f + if (isLTR) { + seekBar.translationX = seekBarMeasuredHeight + hOffset + seekBar.translationY = 0f + } else { + seekBar.translationX = -hOffset + seekBar.translationY = seekBarMeasuredWidth.toFloat() + } + } + VerticalSeekBar.ROTATION_ANGLE_CW_270 -> { + seekBar.rotation = 270f + if (isLTR) { + seekBar.translationX = hOffset + seekBar.translationY = seekBarMeasuredWidth.toFloat() + } else { + seekBar.translationX = -(seekBarMeasuredHeight + hOffset) + seekBar.translationY = 0f + } + } + } + } + } + + private fun useViewRotation(): Boolean { + val seekBar = childSeekBar + return seekBar?.useViewRotation() ?: false + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt new file mode 100755 index 000000000..4cdc562dc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatColor + +class AccentBgTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + private var radius = 0 + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AccentBgTextView) + radius = typedArray.getDimensionPixelOffset(R.styleable.AccentBgTextView_radius, radius) + typedArray.recycle() + upBackground() + } + + fun setRadius(radius: Int) { + this.radius = radius.dpToPx() + upBackground() + } + + private fun upBackground() { + val accentColor = if (isInEditMode) { + context.getCompatColor(R.color.accent) + } else { + ThemeStore.accentColor(context) + } + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setDefaultBgColor(accentColor) + .setPressedBgColor(ColorUtils.darkenColor(accentColor)) + .create() + setTextColor( + if (ColorUtils.isColorLight(accentColor)) { + Color.BLACK + } else { + Color.WHITE + } + ) + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt new file mode 100755 index 000000000..34e504584 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt @@ -0,0 +1,60 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatColor + +class AccentStrokeTextView(context: Context, attrs: AttributeSet) : + AppCompatTextView(context, attrs) { + + private var radius = 3.dpToPx() + private val isBottomBackground: Boolean + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AccentStrokeTextView) + radius = typedArray.getDimensionPixelOffset(R.styleable.StrokeTextView_radius, radius) + isBottomBackground = + typedArray.getBoolean(R.styleable.StrokeTextView_isBottomBackground, false) + typedArray.recycle() + upStyle() + } + + private fun upStyle() { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val disableColor = if (isBottomBackground) { + if (isLight) { + context.getCompatColor(R.color.md_light_disabled) + } else { + context.getCompatColor(R.color.md_dark_disabled) + } + } else { + context.getCompatColor(R.color.disabled) + } + val accentColor = if (isInEditMode) { + context.getCompatColor(R.color.accent) + } else { + ThemeStore.accentColor(context) + } + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dpToPx()) + .setDisabledStrokeColor(disableColor) + .setDefaultStrokeColor(accentColor) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(accentColor) + .setDisabledColor(disableColor) + .create() + ) + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt new file mode 100755 index 000000000..8dee26350 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt @@ -0,0 +1,21 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.getCompatColor + +class AccentTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + init { + if (!isInEditMode) { + setTextColor(context.accentColor) + } else { + setTextColor(context.getCompatColor(R.color.accent)) + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt new file mode 100755 index 000000000..262c7c8e5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt @@ -0,0 +1,80 @@ +package io.legado.app.ui.widget.text + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint +import io.legado.app.utils.gone +import io.legado.app.utils.visible + + +@Suppress("unused") +class AutoCompleteTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatAutoCompleteTextView(context, attrs) { + + var delCallBack: ((value: String) -> Unit)? = null + + init { + applyTint(context.accentColor) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + isLocalePreferredLineHeightForMinimumUsed = false + } + } + + override fun enoughToFilter(): Boolean { + return true + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event?.action == MotionEvent.ACTION_DOWN) { + showDropDown() + } + return super.onTouchEvent(event) + } + + fun setFilterValues(values: List?) { + values?.let { + setAdapter(MyAdapter(context, values)) + } + } + + fun setFilterValues(vararg value: String) { + setAdapter(MyAdapter(context, value.toMutableList())) + } + + inner class MyAdapter(context: Context, values: List) : + ArrayAdapter(context, android.R.layout.simple_dropdown_item_1line, values) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: LayoutInflater.from(context) + .inflate(R.layout.item_1line_text_and_del, parent, false) + val textView = view.findViewById(R.id.text_view) + textView.text = getItem(position) + val ivDelete = view.findViewById(R.id.iv_delete) + if (delCallBack != null) ivDelete.visible() else ivDelete.gone() + ivDelete.setOnClickListener { + getItem(position)?.let { + remove(it) + delCallBack?.invoke(it) + showDropDown() + } + } + return view + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt b/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt new file mode 100755 index 000000000..f9e390644 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt @@ -0,0 +1,236 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.text.TextUtils +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.FrameLayout.LayoutParams +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.invisible +import io.legado.app.utils.visible + + +/** + * Created by milad heydari on 5/6/2016. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class BadgeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + var isHideOnNull = true + set(hideOnNull) { + field = hideOnNull + text = text + } + private var radius: Float = 0.toFloat() + private var flatangle: Boolean + + val badgeCount: Int? + get() { + if (text == null) { + return null + } + val text = text.toString() + return kotlin.runCatching { + Integer.parseInt(text) + }.getOrNull() + } + + var badgeGravity: Int + get() { + val params = layoutParams as LayoutParams + return params.gravity + } + set(gravity) { + val params = layoutParams as LayoutParams + params.gravity = gravity + layoutParams = params + } + + val badgeMargin: IntArray + get() { + val params = layoutParams as LayoutParams + return intArrayOf( + params.leftMargin, + params.topMargin, + params.rightMargin, + params.bottomMargin + ) + } + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BadgeView) + val radios = + typedArray.getDimensionPixelOffset(R.styleable.BadgeView_radius, 8) + flatangle = + typedArray.getBoolean(R.styleable.BadgeView_up_flat_angle, false) + typedArray.recycle() + + if (layoutParams !is LayoutParams) { + val layoutParams = LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + setLayoutParams(layoutParams) + } + + //setTypeface(Typeface.DEFAULT_BOLD); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) + setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f)) + radius = radios.toFloat() + + // set default background + setBackground(radius, context.accentColor) + + gravity = Gravity.CENTER + + // default values + isHideOnNull = true + setBadgeCount(0) + minWidth = dip2Px(16f) + minHeight = dip2Px(16f) + } + + override fun setBackgroundColor(color: Int) { + val background = background + if (background is ShapeDrawable && background.paint.color == color) { + return + } + setBackground(radius, color) + } + + fun setBackground(dipRadius: Float, badgeColor: Int) { + val radius = dip2Px(dipRadius).toFloat() + val radiusArray = + floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius) + if (flatangle) { + radiusArray.fill(0f, 0, 3) + } + + val roundRect = RoundRectShape(radiusArray, null, null) + val bgDrawable = ShapeDrawable(roundRect) + bgDrawable.paint.color = badgeColor + background = bgDrawable + setTextColor( + if (ColorUtils.isColorLight(badgeColor)) { + Color.BLACK + } else { + Color.WHITE + } + ) + } + + /** + * @see android.widget.TextView.setText + */ + override fun setText(text: CharSequence, type: BufferType) { + if (isHideOnNull && TextUtils.isEmpty(text)) { + invisible() + } else { + visible() + } + super.setText(text, type) + } + + fun setBadgeCount(count: Int) { + text = if (count == 0) "" else count.toString() + } + + fun setHighlight(highlight: Boolean) { + if (highlight) { + setBackgroundColor(context.accentColor) + } else { + setBackgroundColor(context.getCompatColor(R.color.darker_gray)) + } + } + + fun setBadgeMargin(dipMargin: Int) { + setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin) + } + + fun setBadgeMargin( + leftDipMargin: Int, + topDipMargin: Int, + rightDipMargin: Int, + bottomDipMargin: Int + ) { + val params = layoutParams as LayoutParams + params.leftMargin = dip2Px(leftDipMargin.toFloat()) + params.topMargin = dip2Px(topDipMargin.toFloat()) + params.rightMargin = dip2Px(rightDipMargin.toFloat()) + params.bottomMargin = dip2Px(bottomDipMargin.toFloat()) + layoutParams = params + } + + fun incrementBadgeCount(increment: Int) { + val count = badgeCount + if (count == null) { + setBadgeCount(increment) + } else { + setBadgeCount(increment + count) + } + } + + fun decrementBadgeCount(decrement: Int) { + incrementBadgeCount(-decrement) + } + + /** + * Attach the BadgeView to the target view + * @param target the view to attach the BadgeView + */ + fun setTargetView(target: View?) { + if (parent != null) { + (parent as ViewGroup).removeView(this) + } + + if (target == null) { + return + } + + if (target.parent is FrameLayout) { + (target.parent as FrameLayout).addView(this) + + } else if (target.parent is ViewGroup) { + // use a new FrameLayout container for adding badge + val parentContainer = target.parent as ViewGroup + val groupIndex = parentContainer.indexOfChild(target) + parentContainer.removeView(target) + + val badgeContainer = FrameLayout(context) + val parentLayoutParams = target.layoutParams + + badgeContainer.layoutParams = parentLayoutParams + target.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams) + badgeContainer.addView(target) + + badgeContainer.addView(this) + } + + } + + /** + * converts dip to px + */ + private fun dip2Px(dip: Float): Int { + return (dip * context.resources.displayMetrics.density + 0.5f).toInt() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/BevelLabelView.kt b/app/src/main/java/io/legado/app/ui/widget/text/BevelLabelView.kt new file mode 100755 index 000000000..d17153e48 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/BevelLabelView.kt @@ -0,0 +1,352 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.IntDef +import io.legado.app.R +import io.legado.app.lib.theme.accentColor + +/** + * 斜角标签 + */ +@Suppress("unused") +class BevelLabelView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + + companion object { + const val MODE_LEFT_TOP = 0 + const val MODE_RIGHT_TOP = 1 + const val MODE_LEFT_BOTTOM = 2 + const val MODE_RIGHT_BOTTOM = 3 + const val MODE_LEFT_TOP_FILL = 4 + const val MODE_RIGHT_TOP_FILL = 5 + const val MODE_LEFT_BOTTOM_FILL = 6 + const val MODE_RIGHT_BOTTOM_FILL = 7 + } + + private var mBgColor: Int + private var mText: String + private var mTextSize: Int + private var mTextColor: Int + private var mLength: Int + private var mCorner: Int + private var mMode: Int + private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private var path: Path = Path() + private var mWidth = 0 + private var mHeight: Int = 0 + private var mRotate = 45 //因为默认模式是1,所以这时是45度 + + private var mX: Int = 0 + private var mY: Int = 0 + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BevelLabelView) + mBgColor = typedArray.getColor( + R.styleable.BevelLabelView_label_bg_color, + context.accentColor + ) //默认红色 + mText = typedArray.getString(R.styleable.BevelLabelView_label_text) ?: "" + mTextSize = + typedArray.getDimensionPixelOffset( + R.styleable.BevelLabelView_label_text_size, + sp2px(11) + ) + mTextColor = typedArray.getColor(R.styleable.BevelLabelView_label_text_color, Color.WHITE) + mLength = + typedArray.getDimensionPixelOffset(R.styleable.BevelLabelView_label_length, dip2px(40)) + mCorner = typedArray.getDimensionPixelOffset(R.styleable.BevelLabelView_label_corner, 0) + mMode = typedArray.getInt(R.styleable.BevelLabelView_label_mode, 1) + mPaint.isAntiAlias = true + typedArray.recycle() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + mWidth = MeasureSpec.getSize(widthMeasureSpec) + mHeight = mWidth + } + + override fun onDraw(canvas: Canvas) { + mPaint.color = mBgColor + drawBackgroundText(canvas) + } + + fun setMode(@BevelLabelMode mode: Int) { + mMode = mode + invalidate() + } + + fun setTextColor(@ColorInt color: Int) { + mTextColor = color + invalidate() + } + + fun setBgColor(@ColorInt color: Int) { + mBgColor = color + invalidate() + } + + private fun drawBackgroundText(canvas: Canvas) { + check(mWidth == mHeight) { + "width must equal to height" //标签view 是一个正方形, + } + when (mMode) { + MODE_LEFT_TOP -> { + mCorner = 0 //没有铺满的时候mCorner要归零; + leftTopMeasure() + getLeftTop() + } + MODE_RIGHT_TOP -> { + mCorner = 0 + rightTopMeasure() + getRightTop() + } + MODE_LEFT_BOTTOM -> { + mCorner = 0 + leftBottomMeasure() + getLeftBottom() + } + MODE_RIGHT_BOTTOM -> { + mCorner = 0 + rightBottomMeasure() + getRightBottom() + } + MODE_LEFT_TOP_FILL -> { + leftTopMeasure() + getLeftTopFill() + if (mCorner != 0) { + canvas.drawPath(path, mPaint) + getLeftTop() + } + } + MODE_RIGHT_TOP_FILL -> { + rightTopMeasure() + getRightTopFill() + if (mCorner != 0) { + canvas.drawPath(path, mPaint) + getRightTop() + } + } + MODE_LEFT_BOTTOM_FILL -> { + leftBottomMeasure() + getLeftBottomFill() + if (mCorner != 0) { + canvas.drawPath(path, mPaint) + getLeftBottom() + } + } + MODE_RIGHT_BOTTOM_FILL -> { + rightBottomMeasure() + getRightBottomFill() + if (mCorner != 0) { + canvas.drawPath(path, mPaint) + getRightBottom() + } + } + else -> {} + } + canvas.drawPath(path, mPaint) + mPaint.textSize = mTextSize.toFloat() + mPaint.textAlign = Paint.Align.CENTER + mPaint.color = mTextColor + canvas.translate(mX.toFloat(), mY.toFloat()) + canvas.rotate(mRotate.toFloat()) + val baseLineY = (-(mPaint.descent() + mPaint.ascent())).toInt() / 2 //基线中间点的y轴计算公式 + canvas.drawText(mText, 0f, baseLineY.toFloat(), mPaint) + } + + private fun rightBottomMeasure() { + mRotate = -45 + mX = mWidth / 2 + mLength / 4 + mY = mX + } + + private fun leftBottomMeasure() { + mRotate = 45 + mX = mWidth / 2 - mLength / 4 + mY = mHeight / 2 + mLength / 4 + } + + private fun rightTopMeasure() { + mRotate = 45 + mX = mWidth / 2 + mLength / 4 + mY = mHeight / 2 - mLength / 4 + } + + private fun leftTopMeasure() { + mRotate = -45 + mX = mWidth / 2 - mLength / 4 + mY = mX + } + + //左上角铺满 + private fun getLeftTopFill() { + if (mCorner != 0) { + path.addRoundRect( + 0f, + 0f, + (mWidth / 2).toFloat(), + (mHeight / 2).toFloat(), + floatArrayOf(mCorner.toFloat(), mCorner.toFloat(), 0f, 0f, 0f, 0f, 0f, 0f), + Path.Direction.CW + ) + } else { + path.moveTo(0f, 0f) + path.lineTo(mWidth.toFloat(), 0f) + path.lineTo(0f, mHeight.toFloat()) + path.close() + } + } + + //左上角不铺满 + private fun getLeftTop() { + path.moveTo(if (mCorner != 0) mCorner.toFloat() else (mWidth - mLength).toFloat(), 0f) + path.lineTo(mWidth.toFloat(), 0f) + path.lineTo(0f, mHeight.toFloat()) + path.lineTo(0f, if (mCorner != 0) mCorner.toFloat() else (mHeight - mLength).toFloat()) + path.close() + } + + //左下角铺满 + private fun getLeftBottomFill() { + if (mCorner != 0) { + path.addRoundRect( + 0f, + (mHeight / 2).toFloat(), + (mWidth / 2).toFloat(), + mHeight.toFloat(), + floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, mCorner.toFloat(), mCorner.toFloat()), + Path.Direction.CW + ) + } else { + path.moveTo(0f, 0f) + path.lineTo(mWidth.toFloat(), mHeight.toFloat()) + path.lineTo(0f, mHeight.toFloat()) + path.close() + } + } + + + //左下角不铺满 + private fun getLeftBottom() { + path.moveTo(0f, 0f) + path.lineTo(mWidth.toFloat(), mHeight.toFloat()) + path.lineTo( + if (mCorner != 0) mCorner.toFloat() else (mWidth - mLength).toFloat(), + mHeight.toFloat() + ) + path.lineTo(0f, if (mCorner != 0) (mHeight - mCorner).toFloat() else mLength.toFloat()) + path.close() + } + + //右上角铺满 + private fun getRightTopFill() { + if (mCorner != 0) { + path.addRoundRect( + (mWidth / 2).toFloat(), + 0f, + mWidth.toFloat(), + (mHeight / 2).toFloat(), + floatArrayOf(0f, 0f, mCorner.toFloat(), mCorner.toFloat(), 0f, 0f, 0f, 0f), + Path.Direction.CW + ) + } else { + path.moveTo(0f, 0f) + path.lineTo(mWidth.toFloat(), 0f) + path.lineTo(mWidth.toFloat(), mHeight.toFloat()) + path.close() + } + } + + //右上角不铺满 + private fun getRightTop() { + path.moveTo(0f, 0f) + path.lineTo(if (mCorner != 0) (mWidth - mCorner).toFloat() else mLength.toFloat(), 0f) + path.lineTo( + mWidth.toFloat(), + if (mCorner != 0) mCorner.toFloat() else (mHeight - mLength).toFloat() + ) + path.lineTo(mWidth.toFloat(), mHeight.toFloat()) + path.close() + } + + //右下角铺满 + private fun getRightBottomFill() { + if (mCorner != 0) { + path.addRoundRect( + (mWidth / 2).toFloat(), + (mHeight / 2).toFloat(), + mWidth.toFloat(), + mHeight.toFloat(), + floatArrayOf(0f, 0f, 0f, 0f, mCorner.toFloat(), mCorner.toFloat(), 0f, 0f), + Path.Direction.CW + ) + } else { + path.moveTo(mWidth.toFloat(), 0f) + path.lineTo(mWidth.toFloat(), mHeight.toFloat()) + path.lineTo(0f, mHeight.toFloat()) + path.close() + } + } + + //右下角不铺满 + private fun getRightBottom() { + path.moveTo(mWidth.toFloat(), 0f) + path.lineTo( + mWidth.toFloat(), + if (mCorner != 0) (mHeight - mCorner).toFloat() else mLength.toFloat() + ) + path.lineTo( + if (mCorner != 0) (mWidth - mCorner).toFloat() else mLength.toFloat(), + mHeight.toFloat() + ) + path.lineTo(0f, mHeight.toFloat()) + path.close() + } + + + /** + * @param sp 转换大小 + */ + @Suppress("SameParameterValue") + private fun sp2px(sp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp.toFloat(), + resources.displayMetrics + ) + .toInt() + } + + /** + * @param dip 转换大小 + */ + @Suppress("SameParameterValue") + private fun dip2px(dip: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dip.toFloat(), + resources.displayMetrics + ) + .toInt() + } + + @Target(AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.SOURCE) + @IntDef( + MODE_LEFT_BOTTOM, MODE_LEFT_BOTTOM_FILL, MODE_LEFT_TOP, MODE_LEFT_TOP_FILL, + MODE_RIGHT_BOTTOM, MODE_RIGHT_BOTTOM_FILL, MODE_RIGHT_TOP, MODE_RIGHT_TOP_FILL + ) + annotation class BevelLabelMode + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/EditEntity.kt b/app/src/main/java/io/legado/app/ui/widget/text/EditEntity.kt new file mode 100755 index 000000000..e715b69c1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/EditEntity.kt @@ -0,0 +1,31 @@ +package io.legado.app.ui.widget.text + +import splitties.init.appCtx + +data class EditEntity( + var key: String, + var value: String?, + var hint: String, + val viewType: Int = 0 +) { + + constructor( + key: String, + value: String?, + hint: Int, + viewType: Int = 0 + ) : this( + key, + value, + appCtx.getString(hint), + viewType + ) + + @Suppress("unused") + object ViewType { + + const val checkBox = 1 + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/MultilineTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/MultilineTextView.kt new file mode 100755 index 000000000..acaa6aa0a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/MultilineTextView.kt @@ -0,0 +1,28 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView + +class MultilineTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isFallbackLineSpacing = false + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + calculateLines(heightSize) + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + private fun calculateLines(measuredHeight: Int) { + val lHeight = lineHeight + val lines = measuredHeight / lHeight + setLines(lines) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt new file mode 100755 index 000000000..12a99638b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt @@ -0,0 +1,18 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.lib.theme.ThemeStore + +/** + * @author Aidan Follestad (afollestad) + */ +@Suppress("unused") +class PrimaryTextView(context: Context, attrs: AttributeSet) : + AppCompatTextView(context, attrs) { + + init { + setTextColor(ThemeStore.textColorPrimary(context)) + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/ScrollMultiAutoCompleteTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/ScrollMultiAutoCompleteTextView.kt new file mode 100755 index 000000000..18a5f6e92 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/ScrollMultiAutoCompleteTextView.kt @@ -0,0 +1,275 @@ +package io.legado.app.ui.widget.text + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.ViewConfiguration +import android.view.animation.Interpolator +import android.widget.OverScroller +import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView +import androidx.core.view.ViewCompat +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** + * 嵌套惯性滚动 MultiAutoCompleteTextView + */ +open class ScrollMultiAutoCompleteTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatMultiAutoCompleteTextView(context, attrs) { + + //是否到顶或者到底的标志 + private var disallowIntercept = true + + private val scrollStateIdle = 0 + private val scrollStateDragging = 1 + val scrollStateSettling = 2 + + private val mViewFling: ViewFling by lazy { ViewFling() } + private val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() } + private var mScrollState = scrollStateIdle + private var mLastTouchY: Int = 0 + private var mTouchSlop: Int = 0 + private var mMinFlingVelocity: Int = 0 + private var mMaxFlingVelocity: Int = 0 + + //滑动距离的最大边界 + private var mOffsetHeight: Int = 0 + + //f(x) = (x-1)^5 + 1 + private val sQuinticInterpolator = Interpolator { + var t = it + t -= 1.0f + t * t * t * t * t + 1.0f + } + + private val gestureDetector = GestureDetector(context, + object : GestureDetector.SimpleOnGestureListener() { + + override fun onDown(e: MotionEvent): Boolean { + disallowIntercept = true + return super.onDown(e) + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + val y = scrollY + distanceY + if (y < 0 || y > mOffsetHeight) { + disallowIntercept = false + //这里触发父布局或祖父布局的滑动事件 + parent.requestDisallowInterceptTouchEvent(false) + } else { + disallowIntercept = true + } + return true + } + + }) + + init { + val vc = ViewConfiguration.get(context) + mTouchSlop = vc.scaledTouchSlop + mMinFlingVelocity = vc.scaledMinimumFlingVelocity + mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + isLocalePreferredLineHeightForMinimumUsed = false + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + initOffsetHeight() + } + + override fun onTextChanged( + text: CharSequence, + start: Int, + lengthBefore: Int, + lengthAfter: Int + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + initOffsetHeight() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (lineCount > maxLines) { + gestureDetector.onTouchEvent(event) + } + velocityTracker.addMovement(event) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + setScrollState(scrollStateIdle) + mLastTouchY = (event.y + 0.5f).toInt() + } + MotionEvent.ACTION_MOVE -> { + val y = (event.y + 0.5f).toInt() + var dy = mLastTouchY - y + if (mScrollState != scrollStateDragging) { + var startScroll = false + + if (abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop + } else { + dy += mTouchSlop + } + startScroll = true + } + if (startScroll) { + setScrollState(scrollStateDragging) + } + } + if (mScrollState == scrollStateDragging) { + mLastTouchY = y + } + } + MotionEvent.ACTION_UP -> { + velocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat()) + val yVelocity = velocityTracker.yVelocity + if (abs(yVelocity) > mMinFlingVelocity) { + mViewFling.fling(-yVelocity.toInt()) + } else { + setScrollState(scrollStateIdle) + } + resetTouch() + } + MotionEvent.ACTION_CANCEL -> { + resetTouch() + } + } + return super.dispatchTouchEvent(event) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + val result = super.onTouchEvent(event) + //如果是需要拦截,则再拦截,这个方法会在onScrollChanged方法之后再调用一次 + if (disallowIntercept && lineCount > maxLines) { + parent.requestDisallowInterceptTouchEvent(true) + } + + return result + } + + override fun scrollTo(x: Int, y: Int) { + super.scrollTo(x, min(y, mOffsetHeight)) + } + + private fun initOffsetHeight() { + val mLayoutHeight: Int + + //获得内容面板 + val mLayout = layout ?: return + //获得内容面板的高度 + mLayoutHeight = mLayout.height + //获取上内边距 + val paddingTop: Int = totalPaddingTop + //获取下内边距 + val paddingBottom: Int = totalPaddingBottom + + //获得控件的实际高度 + val mHeight: Int = measuredHeight + + //计算滑动距离的边界 + mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight + if (mOffsetHeight <= 0) { + scrollTo(0, 0) + } + } + + private fun resetTouch() { + velocityTracker.clear() + } + + private fun setScrollState(state: Int) { + if (state == mScrollState) { + return + } + mScrollState = state + if (state != scrollStateSettling) { + mViewFling.stop() + } + } + + /** + * 惯性滚动 + */ + private inner class ViewFling : Runnable { + + private var mLastFlingY = 0 + private val mScroller: OverScroller = OverScroller(context, sQuinticInterpolator) + private var mEatRunOnAnimationRequest = false + private var mReSchedulePostAnimationCallback = false + + override fun run() { + disableRunOnAnimationRequests() + val scroller = mScroller + if (scroller.computeScrollOffset()) { + val y = scroller.currY + val dy = y - mLastFlingY + mLastFlingY = y + if (dy < 0 && scrollY > 0) { + scrollBy(0, max(dy, -scrollY)) + } else if (dy > 0 && scrollY < mOffsetHeight) { + scrollBy(0, min(dy, mOffsetHeight - scrollY)) + } + postOnAnimation() + } + enableRunOnAnimationRequests() + } + + fun fling(velocityY: Int) { + mLastFlingY = 0 + setScrollState(scrollStateSettling) + mScroller.fling( + 0, + 0, + 0, + velocityY, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + Integer.MIN_VALUE, + Integer.MAX_VALUE + ) + postOnAnimation() + } + + fun stop() { + removeCallbacks(this) + mScroller.abortAnimation() + } + + private fun disableRunOnAnimationRequests() { + mReSchedulePostAnimationCallback = false + mEatRunOnAnimationRequest = true + } + + private fun enableRunOnAnimationRequests() { + mEatRunOnAnimationRequest = false + if (mReSchedulePostAnimationCallback) { + postOnAnimation() + } + } + + @Suppress("DEPRECATION") + fun postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true + } else { + removeCallbacks(this) + ViewCompat.postOnAnimation(this@ScrollMultiAutoCompleteTextView, this) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/ScrollTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/ScrollTextView.kt new file mode 100755 index 000000000..948146935 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/ScrollTextView.kt @@ -0,0 +1,272 @@ +package io.legado.app.ui.widget.text + +import android.annotation.SuppressLint +import android.content.Context +import android.text.method.LinkMovementMethod +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.ViewConfiguration +import android.view.animation.Interpolator +import android.widget.OverScroller +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.ViewCompat +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + + +/** + * 嵌套惯性滚动 TextView + */ +class ScrollTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + //是否到顶或者到底的标志 + private var disallowIntercept = true + + private val scrollStateIdle = 0 + private val scrollStateDragging = 1 + val scrollStateSettling = 2 + + private val mViewFling: ViewFling by lazy { ViewFling() } + private val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() } + private var mScrollState = scrollStateIdle + private var mLastTouchY: Int = 0 + private var mTouchSlop: Int = 0 + private var mMinFlingVelocity: Int = 0 + private var mMaxFlingVelocity: Int = 0 + + //滑动距离的最大边界 + private var mOffsetHeight: Int = 0 + + //f(x) = (x-1)^5 + 1 + private val sQuinticInterpolator = Interpolator { + var t = it + t -= 1.0f + t * t * t * t * t + 1.0f + } + + private val gestureDetector = GestureDetector(context, + object : GestureDetector.SimpleOnGestureListener() { + + override fun onDown(e: MotionEvent): Boolean { + disallowIntercept = true + return super.onDown(e) + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { + val y = scrollY + distanceY + if (y < 0 || y > mOffsetHeight) { + disallowIntercept = false + //这里触发父布局或祖父布局的滑动事件 + parent.requestDisallowInterceptTouchEvent(false) + } else { + disallowIntercept = true + } + return true + } + + }) + + init { + val vc = ViewConfiguration.get(context) + mTouchSlop = vc.scaledTouchSlop + mMinFlingVelocity = vc.scaledMinimumFlingVelocity + mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + movementMethod = LinkMovementMethod.getInstance() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + initOffsetHeight() + } + + override fun onTextChanged( + text: CharSequence, + start: Int, + lengthBefore: Int, + lengthAfter: Int + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + initOffsetHeight() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (lineCount > maxLines) { + gestureDetector.onTouchEvent(event) + } + velocityTracker.addMovement(event) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + setScrollState(scrollStateIdle) + mLastTouchY = (event.y + 0.5f).toInt() + } + MotionEvent.ACTION_MOVE -> { + val y = (event.y + 0.5f).toInt() + var dy = mLastTouchY - y + if (mScrollState != scrollStateDragging) { + var startScroll = false + + if (abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop + } else { + dy += mTouchSlop + } + startScroll = true + } + if (startScroll) { + setScrollState(scrollStateDragging) + } + } + if (mScrollState == scrollStateDragging) { + mLastTouchY = y + } + } + MotionEvent.ACTION_UP -> { + velocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat()) + val yVelocity = velocityTracker.yVelocity + if (abs(yVelocity) > mMinFlingVelocity) { + mViewFling.fling(-yVelocity.toInt()) + } else { + setScrollState(scrollStateIdle) + } + resetTouch() + } + MotionEvent.ACTION_CANCEL -> { + resetTouch() + } + } + return super.dispatchTouchEvent(event) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + val result = super.onTouchEvent(event) + //如果是需要拦截,则再拦截,这个方法会在onScrollChanged方法之后再调用一次 + if (disallowIntercept && lineCount > maxLines) { + parent.requestDisallowInterceptTouchEvent(true) + } + + return result + } + + override fun scrollTo(x: Int, y: Int) { + super.scrollTo(x, min(y, mOffsetHeight)) + } + + private fun initOffsetHeight() { + val mLayoutHeight: Int + + //获得内容面板 + val mLayout = layout ?: return + //获得内容面板的高度 + mLayoutHeight = mLayout.height + //获取上内边距 + val paddingTop: Int = totalPaddingTop + //获取下内边距 + val paddingBottom: Int = totalPaddingBottom + + //获得控件的实际高度 + val mHeight: Int = measuredHeight + + //计算滑动距离的边界 + mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight + if (mOffsetHeight <= 0) { + scrollTo(0, 0) + } + } + + private fun resetTouch() { + velocityTracker.clear() + } + + private fun setScrollState(state: Int) { + if (state == mScrollState) { + return + } + mScrollState = state + if (state != scrollStateSettling) { + mViewFling.stop() + } + } + + /** + * 惯性滚动 + */ + private inner class ViewFling : Runnable { + + private var mLastFlingY = 0 + private val mScroller: OverScroller = OverScroller(context, sQuinticInterpolator) + private var mEatRunOnAnimationRequest = false + private var mReSchedulePostAnimationCallback = false + + override fun run() { + disableRunOnAnimationRequests() + val scroller = mScroller + if (scroller.computeScrollOffset()) { + val y = scroller.currY + val dy = y - mLastFlingY + mLastFlingY = y + if (dy < 0 && scrollY > 0) { + scrollBy(0, max(dy, -scrollY)) + } else if (dy > 0 && scrollY < mOffsetHeight) { + scrollBy(0, min(dy, mOffsetHeight - scrollY)) + } + postOnAnimation() + } + enableRunOnAnimationRequests() + } + + fun fling(velocityY: Int) { + mLastFlingY = 0 + setScrollState(scrollStateSettling) + mScroller.fling( + 0, + 0, + 0, + velocityY, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + Integer.MIN_VALUE, + Integer.MAX_VALUE + ) + postOnAnimation() + } + + fun stop() { + removeCallbacks(this) + mScroller.abortAnimation() + } + + private fun disableRunOnAnimationRequests() { + mReSchedulePostAnimationCallback = false + mEatRunOnAnimationRequest = true + } + + private fun enableRunOnAnimationRequests() { + mEatRunOnAnimationRequest = false + if (mReSchedulePostAnimationCallback) { + postOnAnimation() + } + } + + @Suppress("DEPRECATION") + fun postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true + } else { + removeCallbacks(this) + ViewCompat.postOnAnimation(this@ScrollTextView, this) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt new file mode 100755 index 000000000..6e18856bf --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt @@ -0,0 +1,18 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.lib.theme.secondaryTextColor + +/** + * @author Aidan Follestad (afollestad) + */ +@Suppress("unused") +class SecondaryTextView(context: Context, attrs: AttributeSet) : + AppCompatTextView(context, attrs) { + + init { + setTextColor(context.secondaryTextColor) + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt new file mode 100755 index 000000000..a5aa3db42 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt @@ -0,0 +1,89 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.* +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatColor + +@Suppress("unused") +open class StrokeTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + private var radius = 1.dpToPx() + private val isBottomBackground: Boolean + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.StrokeTextView) + radius = typedArray.getDimensionPixelOffset(R.styleable.StrokeTextView_radius, radius) + isBottomBackground = + typedArray.getBoolean(R.styleable.StrokeTextView_isBottomBackground, false) + typedArray.recycle() + upBackground() + } + + fun setRadius(radius: Int) { + this.radius = radius.dpToPx() + upBackground() + } + + private fun upBackground() { + when { + isInEditMode -> { + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dpToPx()) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(context.getCompatColor(R.color.secondaryText)) + .setSelectedStrokeColor(context.getCompatColor(R.color.accent)) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(context.getCompatColor(R.color.secondaryText)) + .setSelectedColor(context.getCompatColor(R.color.accent)) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + isBottomBackground -> { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dpToPx()) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(context.getPrimaryTextColor(isLight)) + .setSelectedStrokeColor(context.accentColor) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(context.getPrimaryTextColor(isLight)) + .setSelectedColor(context.accentColor) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + else -> { + background = Selector.shapeBuild() + .setCornerRadius(radius) + .setStrokeWidth(1.dpToPx()) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(ThemeStore.textColorSecondary(context)) + .setSelectedStrokeColor(ThemeStore.accentColor(context)) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(ThemeStore.textColorSecondary(context)) + .setSelectedColor(ThemeStore.accentColor(context)) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt b/app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt new file mode 100755 index 000000000..015e848c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt @@ -0,0 +1,18 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.textfield.TextInputLayout +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore + +class TextInputLayout(context: Context, attrs: AttributeSet?) : TextInputLayout(context, attrs) { + + init { + if (!isInEditMode) { + defaultHintTextColor = + Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create() + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/ACache.kt b/app/src/main/java/io/legado/app/utils/ACache.kt new file mode 100755 index 000000000..4a714896f --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ACache.kt @@ -0,0 +1,790 @@ +//Copyright (c) 2017. 章钦豪. All rights reserved. +package io.legado.app.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.PixelFormat +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import org.json.JSONArray +import org.json.JSONObject +import splitties.init.appCtx +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable +import java.util.Collections +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong +import kotlin.math.min + + +/** + * 本地缓存 + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) { + + companion object { + const val TIME_HOUR = 60 * 60 + const val TIME_DAY = TIME_HOUR * 24 + private const val MAX_SIZE = 1000 * 1000 * 50 // 50 mb + private const val MAX_COUNT = Integer.MAX_VALUE // 不限制存放数据的数量 + private val mInstanceMap = HashMap() + + @JvmOverloads + fun get( + cacheName: String = "ACache", + maxSize: Long = MAX_SIZE.toLong(), + maxCount: Int = MAX_COUNT, + cacheDir: Boolean = true + ): ACache { + val f = + if (cacheDir) File(appCtx.cacheDir, cacheName) else File(appCtx.filesDir, cacheName) + return get(f, maxSize, maxCount) + } + + @JvmOverloads + fun get( + cacheDir: File, + maxSize: Long = MAX_SIZE.toLong(), + maxCount: Int = MAX_COUNT + ): ACache { + synchronized(this) { + var manager = mInstanceMap[cacheDir.absoluteFile.toString() + myPid()] + if (manager == null) { + manager = ACache(cacheDir, maxSize, maxCount) + mInstanceMap[cacheDir.absolutePath + myPid()] = manager + } + return manager + } + } + + private fun myPid(): String { + return "_" + android.os.Process.myPid() + } + } + + private var mCache: ACacheManager? = null + + init { + try { + if (!cacheDir.exists() && !cacheDir.mkdirs()) { + DebugLog.i(javaClass.name, "can't make dirs in %s" + cacheDir.absolutePath) + } + mCache = ACacheManager(cacheDir, max_size, max_count) + } catch (e: Exception) { + e.printOnDebug() + } + + } + + // ======================================= + // ============ String数据 读写 ============== + // ======================================= + + /** + * 保存 String数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的String数据 + */ + fun put(key: String, value: String) { + mCache?.let { mCache -> + try { + val file = mCache.newFile(key) + file.writeText(value) + mCache.put(file) + } catch (e: Exception) { + e.printOnDebug() + } + } + } + + /** + * 保存 String数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的String数据 + * @param saveTime 保存的时间,单位:秒 + */ + fun put(key: String, value: String, saveTime: Int) { + if (saveTime == 0) put(key, value) else put( + key, + Utils.newStringWithDateInfo(saveTime, value) + ) + } + + /** + * 读取 String数据 + * + * @return String 数据 + */ + fun getAsString(key: String): String? { + mCache?.let { mCache -> + val file = mCache[key] + if (!file.exists()) + return null + var removeFile = false + try { + val text = file.readText() + if (!Utils.isDue(text)) { + return Utils.clearDateInfo(text) + } else { + removeFile = true + } + } catch (e: IOException) { + e.printOnDebug() + } finally { + if (removeFile) + remove(key) + } + } + return null + } + + // ======================================= + // ========== JSONObject 数据 读写 ========= + // ======================================= + + /** + * 保存 JSONObject数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的JSON数据 + */ + fun put(key: String, value: JSONObject) { + put(key, value.toString()) + } + + /** + * 保存 JSONObject数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的JSONObject数据 + * @param saveTime 保存的时间,单位:秒 + */ + fun put(key: String, value: JSONObject, saveTime: Int) { + put(key, value.toString(), saveTime) + } + + /** + * 读取JSONObject数据 + * + * @return JSONObject数据 + */ + fun getAsJSONObject(key: String): JSONObject? { + val json = getAsString(key) ?: return null + return try { + JSONObject(json) + } catch (e: Exception) { + null + } + } + + // ======================================= + // ============ JSONArray 数据 读写 ============= + // ======================================= + + /** + * 保存 JSONArray数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的JSONArray数据 + */ + fun put(key: String, value: JSONArray) { + put(key, value.toString()) + } + + /** + * 保存 JSONArray数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的JSONArray数据 + * @param saveTime 保存的时间,单位:秒 + */ + fun put(key: String, value: JSONArray, saveTime: Int) { + put(key, value.toString(), saveTime) + } + + /** + * 读取JSONArray数据 + * + * @return JSONArray数据 + */ + fun getAsJSONArray(key: String): JSONArray? { + val json = getAsString(key) + return try { + JSONArray(json) + } catch (e: Exception) { + null + } + + } + + // ======================================= + // ============== byte 数据 读写 ============= + // ======================================= + + /** + * 保存 byte数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的数据 + */ + fun put(key: String, value: ByteArray) { + mCache?.let { mCache -> + val file = mCache.newFile(key) + file.writeBytes(value) + mCache.put(file) + } + } + + /** + * 保存 byte数据 到 缓存中 + * + * @param key 保存的key + * @param value 保存的数据 + * @param saveTime 保存的时间,单位:秒 + */ + fun put(key: String, value: ByteArray, saveTime: Int) { + if (saveTime == 0) put(key, value) + else put(key, Utils.newByteArrayWithDateInfo(saveTime, value)) + } + + /** + * 获取 byte 数据 + * + * @return byte 数据 + */ + fun getAsBinary(key: String): ByteArray? { + mCache?.let { mCache -> + var removeFile = false + try { + val file = mCache[key] + if (!file.exists()) + return null + + val byteArray = file.readBytes() + return if (!Utils.isDue(byteArray)) { + Utils.clearDateInfo(byteArray) + } else { + removeFile = true + null + } + } catch (e: Exception) { + e.printOnDebug() + } finally { + if (removeFile) + remove(key) + } + } + return null + } + + /** + * 保存 Serializable数据到 缓存中 + * + * @param key 保存的key + * @param value 保存的value + * @param saveTime 保存的时间,单位:秒 + */ + @JvmOverloads + fun put(key: String, value: Serializable, saveTime: Int = -1) { + try { + val byteArrayOutputStream = ByteArrayOutputStream() + ObjectOutputStream(byteArrayOutputStream).use { oos -> + oos.writeObject(value) + val data = byteArrayOutputStream.toByteArray() + if (saveTime != -1) { + put(key, data, saveTime) + } else { + put(key, data) + } + } + } catch (e: Exception) { + e.printOnDebug() + } + } + + /** + * 读取 Serializable数据 + * + * @return Serializable 数据 + */ + fun getAsObject(key: String): Any? { + val data = getAsBinary(key) + if (data != null) { + var bis: ByteArrayInputStream? = null + var ois: ObjectInputStream? = null + try { + bis = ByteArrayInputStream(data) + ois = ObjectInputStream(bis) + return ois.readObject() + } catch (e: Exception) { + e.printOnDebug() + } finally { + try { + bis?.close() + } catch (e: IOException) { + e.printOnDebug() + } + + try { + ois?.close() + } catch (e: IOException) { + e.printOnDebug() + } + + } + } + return null + + } + + // ======================================= + // ============== bitmap 数据 读写 ============= + // ======================================= + + /** + * 保存 bitmap 到 缓存中 + * + * @param key 保存的key + * @param value 保存的bitmap数据 + */ + fun put(key: String, value: Bitmap) { + put(key, Utils.bitmap2Bytes(value)) + } + + /** + * 保存 bitmap 到 缓存中 + * + * @param key 保存的key + * @param value 保存的 bitmap 数据 + * @param saveTime 保存的时间,单位:秒 + */ + fun put(key: String, value: Bitmap, saveTime: Int) { + put(key, Utils.bitmap2Bytes(value), saveTime) + } + + /** + * 读取 bitmap 数据 + * + * @return bitmap 数据 + */ + fun getAsBitmap(key: String): Bitmap? { + return if (getAsBinary(key) == null) { + null + } else Utils.bytes2Bitmap(getAsBinary(key)!!) + } + + // ======================================= + // ============= drawable 数据 读写 ============= + // ======================================= + + /** + * 保存 drawable 到 缓存中 + * + * @param key 保存的key + * @param value 保存的drawable数据 + */ + fun put(key: String, value: Drawable) { + put(key, Utils.drawable2Bitmap(value)) + } + + /** + * 保存 drawable 到 缓存中 + * + * @param key 保存的key + * @param value 保存的 drawable 数据 + * @param saveTime 保存的时间,单位:秒 + */ + fun put(key: String, value: Drawable, saveTime: Int) { + put(key, Utils.drawable2Bitmap(value), saveTime) + } + + /** + * 读取 Drawable 数据 + * + * @return Drawable 数据 + */ + fun getAsDrawable(key: String): Drawable? { + return if (getAsBinary(key) == null) { + null + } else Utils.bitmap2Drawable( + Utils.bytes2Bitmap( + getAsBinary(key)!! + ) + ) + } + + /** + * 获取缓存文件 + * + * @return value 缓存的文件 + */ + fun file(key: String): File? { + mCache?.let { mCache -> + try { + val f = mCache.newFile(key) + if (f.exists()) { + return f + } + } catch (e: Exception) { + e.printOnDebug() + } + } + return null + } + + /** + * 移除某个key + * + * @return 是否移除成功 + */ + fun remove(key: String): Boolean { + return mCache?.remove(key) == true + } + + /** + * 清除所有数据 + */ + fun clear() { + mCache?.clear() + } + + /** + * @author 杨福海(michael) www.yangfuhai.com + * @version 1.0 + * title 时间计算工具类 + */ + private object Utils { + + @Suppress("ConstPropertyName") + private const val mSeparator = ' ' + + /** + * 判断缓存的String数据是否到期 + * + * @return true:到期了 false:还没有到期 + */ + fun isDue(str: String): Boolean { + return isDue(str.toByteArray()) + } + + /** + * 判断缓存的byte数据是否到期 + * + * @return true:到期了 false:还没有到期 + */ + fun isDue(data: ByteArray): Boolean { + try { + val text = getDateInfoFromDate(data) + if (text != null && text.size == 2) { + var saveTimeStr = text[0] + while (saveTimeStr.startsWith("0")) { + saveTimeStr = saveTimeStr + .substring(1) + } + val saveTime = java.lang.Long.valueOf(saveTimeStr) + val deleteAfter = java.lang.Long.valueOf(text[1]) + if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) { + return true + } + } + } catch (e: Exception) { + e.printOnDebug() + } + + return false + } + + fun newStringWithDateInfo(second: Int, strInfo: String): String { + return createDateInfo(second) + strInfo + } + + fun newByteArrayWithDateInfo(second: Int, data2: ByteArray): ByteArray { + val data1 = createDateInfo(second).toByteArray() + val retData = ByteArray(data1.size + data2.size) + System.arraycopy(data1, 0, retData, 0, data1.size) + System.arraycopy(data2, 0, retData, data1.size, data2.size) + return retData + } + + fun clearDateInfo(strInfo: String?): String? { + strInfo?.let { + if (hasDateInfo(strInfo.toByteArray())) { + return strInfo.substring(strInfo.indexOf(mSeparator) + 1) + } + } + return strInfo + } + + fun clearDateInfo(data: ByteArray): ByteArray { + return if (hasDateInfo(data)) { + copyOfRange( + data, indexOf(data, mSeparator) + 1, + data.size + ) + } else data + } + + fun hasDateInfo(data: ByteArray?): Boolean { + return (data != null && data.size > 15 && data[13] == '-'.code.toByte() + && indexOf(data, mSeparator) > 14) + } + + fun getDateInfoFromDate(data: ByteArray): Array? { + if (hasDateInfo(data)) { + val saveDate = String(copyOfRange(data, 0, 13)) + val deleteAfter = String( + copyOfRange( + data, 14, + indexOf(data, mSeparator) + ) + ) + return arrayOf(saveDate, deleteAfter) + } + return null + } + + @Suppress("SameParameterValue") + private fun indexOf(data: ByteArray, c: Char): Int { + for (i in data.indices) { + if (data[i] == c.code.toByte()) { + return i + } + } + return -1 + } + + private fun copyOfRange(original: ByteArray, from: Int, to: Int): ByteArray { + val newLength = to - from + require(newLength >= 0) { "$from > $to" } + val copy = ByteArray(newLength) + System.arraycopy( + original, from, copy, 0, + min(original.size - from, newLength) + ) + return copy + } + + private fun createDateInfo(second: Int): String { + val currentTime = StringBuilder(System.currentTimeMillis().toString() + "") + while (currentTime.length < 13) { + currentTime.insert(0, "0") + } + return "$currentTime-$second$mSeparator" + } + + /* + * Bitmap → byte[] + */ + fun bitmap2Bytes(bm: Bitmap): ByteArray { + val byteArrayOutputStream = ByteArrayOutputStream() + bm.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) + return byteArrayOutputStream.toByteArray() + } + + /* + * byte[] → Bitmap + */ + fun bytes2Bitmap(b: ByteArray): Bitmap? { + return if (b.isEmpty()) { + null + } else BitmapFactory.decodeByteArray(b, 0, b.size) + } + + /* + * Drawable → Bitmap + */ + fun drawable2Bitmap(drawable: Drawable): Bitmap { + // 取 drawable 的长宽 + val w = drawable.intrinsicWidth + val h = drawable.intrinsicHeight + // 取 drawable 的颜色格式 + @Suppress("DEPRECATION") + val config = if (drawable.opacity != PixelFormat.OPAQUE) + Bitmap.Config.ARGB_8888 + else + Bitmap.Config.RGB_565 + // 建立对应 bitmap + val bitmap = Bitmap.createBitmap(w, h, config) + // 建立对应 bitmap 的画布 + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, w, h) + // 把 drawable 内容画到画布中 + drawable.draw(canvas) + return bitmap + } + + /* + * Bitmap → Drawable + */ + fun bitmap2Drawable(bm: Bitmap?): Drawable? { + return if (bm == null) { + null + } else BitmapDrawable(appCtx.resources, bm) + } + } + + /** + * @author 杨福海(michael) www.yangfuhai.com + * @version 1.0 + * title 缓存管理器 + */ + open inner class ACacheManager( + private var cacheDir: File, + private val sizeLimit: Long, + private val countLimit: Int + ) { + private val cacheSize: AtomicLong = AtomicLong() + private val cacheCount: AtomicInteger = AtomicInteger() + private val lastUsageDates = Collections + .synchronizedMap(HashMap()) + + init { + calculateCacheSizeAndCacheCount() + } + + /** + * 计算 cacheSize和cacheCount + */ + private fun calculateCacheSizeAndCacheCount() { + Thread { + + try { + var size = 0 + var count = 0 + val cachedFiles = cacheDir.listFiles() + if (cachedFiles != null) { + for (cachedFile in cachedFiles) { + size += calculateSize(cachedFile).toInt() + count += 1 + lastUsageDates[cachedFile] = cachedFile.lastModified() + } + cacheSize.set(size.toLong()) + cacheCount.set(count) + } + } catch (e: Exception) { + e.printOnDebug() + } + + + }.start() + } + + fun put(file: File) { + + try { + var curCacheCount = cacheCount.get() + while (curCacheCount + 1 > countLimit) { + val freedSize = removeNext() + cacheSize.addAndGet(-freedSize) + + curCacheCount = cacheCount.addAndGet(-1) + } + cacheCount.addAndGet(1) + + val valueSize = calculateSize(file) + var curCacheSize = cacheSize.get() + while (curCacheSize + valueSize > sizeLimit) { + val freedSize = removeNext() + curCacheSize = cacheSize.addAndGet(-freedSize) + } + cacheSize.addAndGet(valueSize) + + val currentTime = System.currentTimeMillis() + file.setLastModified(currentTime) + lastUsageDates[file] = currentTime + } catch (e: Exception) { + e.printOnDebug() + } + + } + + operator fun get(key: String): File { + val file = newFile(key) + val currentTime = System.currentTimeMillis() + file.setLastModified(currentTime) + lastUsageDates[file] = currentTime + + return file + } + + fun newFile(key: String): File { + return File(cacheDir, key.hashCode().toString() + "") + } + + fun remove(key: String): Boolean { + val image = get(key) + return image.delete() + } + + fun clear() { + try { + lastUsageDates.clear() + cacheSize.set(0) + val files = cacheDir.listFiles() + if (files != null) { + for (f in files) { + f.delete() + } + } + } catch (e: Exception) { + e.printOnDebug() + } + + } + + /** + * 移除旧的文件 + */ + private fun removeNext(): Long { + try { + if (lastUsageDates.isEmpty()) { + return 0 + } + + var oldestUsage: Long? = null + var mostLongUsedFile: File? = null + val entries = lastUsageDates.entries + synchronized(lastUsageDates) { + for ((key, lastValueUsage) in entries) { + if (mostLongUsedFile == null) { + mostLongUsedFile = key + oldestUsage = lastValueUsage + } else { + if (lastValueUsage < oldestUsage!!) { + oldestUsage = lastValueUsage + mostLongUsedFile = key + } + } + } + } + + var fileSize: Long = 0 + if (mostLongUsedFile != null) { + fileSize = calculateSize(mostLongUsedFile) + if (mostLongUsedFile.delete()) { + lastUsageDates.remove(mostLongUsedFile) + } + } + return fileSize + } catch (e: Exception) { + e.printOnDebug() + return 0 + } + + } + + private fun calculateSize(file: File): Long { + return file.length() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ActivityExtensions.kt b/app/src/main/java/io/legado/app/utils/ActivityExtensions.kt new file mode 100755 index 000000000..680c7d176 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ActivityExtensions.kt @@ -0,0 +1,254 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowInsetsController +import android.view.WindowManager +import android.view.WindowMetrics +import android.widget.FrameLayout +import androidx.annotation.ColorInt +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE +import androidx.fragment.app.DialogFragment +import io.legado.app.R +import io.legado.app.ui.widget.dialog.TextDialog + +inline fun AppCompatActivity.showDialogFragment( + arguments: Bundle.() -> Unit = {} +) { + @Suppress("DEPRECATION") + val dialog = T::class.java.newInstance() + val bundle = Bundle() + bundle.apply(arguments) + dialog.arguments = bundle + dialog.show(supportFragmentManager, T::class.simpleName) +} + +inline fun AppCompatActivity.dismissDialogFragment() { + supportFragmentManager.fragments.forEach { + if (it is T) { + it.dismissAllowingStateLoss() + } + } +} + +fun AppCompatActivity.showDialogFragment(dialogFragment: DialogFragment) { + dialogFragment.show(supportFragmentManager, dialogFragment::class.simpleName) +} + +val WindowManager.windowSize: DisplayMetrics + get() { + val displayMetrics = DisplayMetrics() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowMetrics: WindowMetrics = currentWindowMetrics + val insets = windowMetrics.windowInsets + .getInsetsIgnoringVisibility( + WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() + ) + val windowWidth = windowMetrics.bounds.width() + val windowHeight = windowMetrics.bounds.height() + var insetsWidth = insets.left + insets.right + var insetsHeight = insets.top + insets.bottom + if (windowWidth > windowHeight) { + val tmp = insetsWidth + insetsWidth = insetsHeight + insetsHeight = tmp + } + displayMetrics.widthPixels = windowWidth - insetsWidth + displayMetrics.heightPixels = windowHeight - insetsHeight + } else { + @Suppress("DEPRECATION") + defaultDisplay.getMetrics(displayMetrics) + } + return displayMetrics + } + +@Suppress("DEPRECATION") +fun Activity.fullScreen() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(true) + } + window.decorView.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + window.clearFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + ) + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) +} + +/** + * 设置状态栏颜色 + */ +@Suppress("DEPRECATION") +fun Activity.setStatusBarColorAuto( + @ColorInt color: Int, + isTransparent: Boolean, + fullScreen: Boolean +) { + val isLightBar = ColorUtils.isColorLight(color) + if (fullScreen) { + if (isTransparent) { + window.statusBarColor = Color.TRANSPARENT + } else { + window.statusBarColor = getCompatColor(R.color.status_bar_bag) + } + } else { + window.statusBarColor = color + } + setLightStatusBar(isLightBar) +} + +@SuppressLint("ObsoleteSdkInt") +fun Activity.setLightStatusBar(isLightBar: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.insetsController?.let { + if (isLightBar) { + it.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } else { + it.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) + } + } + } + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val decorView = window.decorView + val systemUiVisibility = decorView.systemUiVisibility + if (isLightBar) { + decorView.systemUiVisibility = + systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } else { + decorView.systemUiVisibility = + systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } + } +} + +/** + * 设置导航栏颜色 + */ +@Suppress("DEPRECATION") +fun Activity.setNavigationBarColorAuto(@ColorInt color: Int) { + val isLightBor = ColorUtils.isColorLight(color) + window.navigationBarColor = color + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.insetsController?.let { + if (isLightBor) { + it.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + ) + } else { + it.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + ) + } + } + } + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val decorView = window.decorView + var systemUiVisibility = decorView.systemUiVisibility + systemUiVisibility = if (isLightBor) { + systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } + decorView.systemUiVisibility = systemUiVisibility + } +} + +fun Activity.keepScreenOn(on: Boolean) { + val isScreenOn = + (window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0 + if (on == isScreenOn) return + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } +} + +fun Activity.toggleSystemBar(show: Boolean) { + WindowCompat.getInsetsController(window, window.decorView).run { + if (show) { + show(WindowInsetsCompat.Type.systemBars()) + } else { + hide(WindowInsetsCompat.Type.systemBars()) + systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } +} + +/////以下方法需要在View完全被绘制出来之后调用,否则判断不了,在比如 onWindowFocusChanged()方法中可以得到正确的结果///// + +/** + * 返回NavigationBar + */ +val Activity.navigationBar: View? + get() { + val viewGroup = (window.decorView as? ViewGroup) ?: return null + for (i in 0 until viewGroup.childCount) { + val child = viewGroup.getChildAt(i) + val childId = child.id + if (childId != View.NO_ID + && resources.getResourceEntryName(childId) == "navigationBarBackground" + ) { + return child + } + } + return null + } + +/** + * 返回NavigationBar是否存在 + */ +val Activity.isNavigationBarExist: Boolean + get() = navigationBar != null + +/** + * 返回NavigationBar高度 + */ +val Activity.navigationBarHeight: Int + @SuppressLint("InternalInsetResource", "DiscouragedApi") + get() { + if (isNavigationBarExist) { + val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + return 0 + } + +/** + * 返回navigationBar位置 + */ +val Activity.navigationBarGravity: Int + get() { + val gravity = (navigationBar?.layoutParams as? FrameLayout.LayoutParams)?.gravity + return gravity ?: Gravity.BOTTOM + } + +/** + * 显示目录help下的帮助文档 + */ +fun AppCompatActivity.showHelp(fileName: String) { + val mdText = String(assets.open("web/help/md/${fileName}.md").readBytes()) + showDialogFragment(TextDialog(getString(R.string.help), mdText, TextDialog.Mode.MD)) +} diff --git a/app/src/main/java/io/legado/app/utils/ActivityResult.kt b/app/src/main/java/io/legado/app/utils/ActivityResult.kt new file mode 100755 index 000000000..06d6dc47a --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ActivityResult.kt @@ -0,0 +1,47 @@ +package io.legado.app.utils + +import androidx.activity.result.contract.ActivityResultContract +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityOptionsCompat +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +fun AppCompatActivity.registerForActivityResult(contract: ActivityResultContract): ActivityResultLauncherAwait { + lateinit var cout: CancellableContinuation + val launcher = registerForActivityResult(contract) { + if (cout.isActive) { + cout.resume(it) + } + } + return object : ActivityResultLauncherAwait() { + override suspend fun launch(input: I, options: ActivityOptionsCompat?): O { + return suspendCancellableCoroutine { + cout = it + launcher.launch(input, options) + } + } + + override fun unregister() { + launcher.unregister() + } + + override fun getContract(): ActivityResultContract { + return launcher.contract + } + } +} + +abstract class ActivityResultLauncherAwait { + + suspend fun launch(input: I): O { + return launch(input, null) + } + + abstract suspend fun launch(input: I, options: ActivityOptionsCompat?): O + + abstract fun unregister() + + abstract fun getContract(): ActivityResultContract + +} diff --git a/app/src/main/java/io/legado/app/utils/ActivityResultContracts.kt b/app/src/main/java/io/legado/app/utils/ActivityResultContracts.kt new file mode 100755 index 000000000..ffb925c53 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ActivityResultContracts.kt @@ -0,0 +1,72 @@ +package io.legado.app.utils + +import android.app.Activity.RESULT_OK +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import splitties.init.appCtx + +fun ActivityResultLauncher.launch() { + launch(null) +} + +class SelectImageContract : ActivityResultContract() { + + private val delegate = ActivityResultContracts.PickVisualMedia() + private var requestCode: Int? = null + private var useFallback = false + + override fun createIntent(context: Context, input: Int?): Intent { + requestCode = input + val intent = Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("image/*") + if (intent.resolveActivity(appCtx.packageManager) == null) { + useFallback = true + val request = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + return delegate.createIntent(context, request) + } + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Result { + val uri = if (useFallback) { + delegate.parseResult(resultCode, intent) + } else if (resultCode == RESULT_OK) { + intent?.data + } else { + null + } + return Result(requestCode, uri) + } + + data class Result( + val requestCode: Int?, + val uri: Uri? = null + ) + +} + +class StartActivityContract(private val cls: Class<*>) : + ActivityResultContract<(Intent.() -> Unit)?, ActivityResult>() { + + override fun createIntent(context: Context, input: (Intent.() -> Unit)?): Intent { + val intent = Intent(context, cls) + input?.let { + intent.apply(input) + } + return intent + } + + override fun parseResult( + resultCode: Int, intent: Intent? + ): ActivityResult { + return ActivityResult(resultCode, intent) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/AlphanumComparator.kt b/app/src/main/java/io/legado/app/utils/AlphanumComparator.kt new file mode 100755 index 000000000..9f92be3f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/AlphanumComparator.kt @@ -0,0 +1,81 @@ +package io.legado.app.utils + + +/** + * 排序比较 + */ +object AlphanumComparator : Comparator { + + + override fun compare(s1: String, s2: String): Int { + var thisMarker = 0 + var thatMarker = 0 + val s1Length = s1.length + val s2Length = s2.length + + while (thisMarker < s1Length && thatMarker < s2Length) { + val thisChunk = getChunk(s1, s1Length, thisMarker) + thisMarker += thisChunk.length + + val thatChunk = getChunk(s2, s2Length, thatMarker) + thatMarker += thatChunk.length + + // If both chunks contain numeric characters, sort them numerically. + var result: Int + if (isDigit(thisChunk[0]) && isDigit(thatChunk[0])) { + // Simple chunk comparison by length. + val thisChunkLength = thisChunk.length + result = thisChunkLength - thatChunk.length + // If equal, the first different number counts. + if (result == 0) { + for (i in 0 until thisChunkLength) { + result = thisChunk[i] - thatChunk[i] + if (result != 0) { + return result + } + } + } + } else { + result = thisChunk.compareTo(thatChunk) + } + + if (result != 0) { + return result + } + } + + return s1Length - s2Length + } + + private fun getChunk(string: String, length: Int, marker: Int): String { + var current = marker + val chunk = StringBuilder() + var c = string[current] + chunk.append(c) + current++ + if (isDigit(c)) { + while (current < length) { + c = string[current] + if (!isDigit(c)) { + break + } + chunk.append(c) + current++ + } + } else { + while (current < length) { + c = string[current] + if (isDigit(c)) { + break + } + chunk.append(c) + current++ + } + } + return chunk.toString() + } + + private fun isDigit(ch: Char): Boolean { + return ch in '0'..'9' + } +} diff --git a/app/src/main/java/io/legado/app/utils/AnimationExtensions.kt b/app/src/main/java/io/legado/app/utils/AnimationExtensions.kt new file mode 100755 index 000000000..d862306f3 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/AnimationExtensions.kt @@ -0,0 +1,15 @@ +package io.legado.app.utils + +import android.content.Context +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import androidx.annotation.AnimRes +import io.legado.app.help.config.AppConfig + +fun loadAnimation(context: Context, @AnimRes id: Int): Animation { + val animation = AnimationUtils.loadAnimation(context, id) + if (AppConfig.isEInkMode) { + animation.duration = 0 + } + return animation +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ArchiveUtils.kt b/app/src/main/java/io/legado/app/utils/ArchiveUtils.kt new file mode 100755 index 000000000..516725174 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ArchiveUtils.kt @@ -0,0 +1,110 @@ +package io.legado.app.utils + +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import io.legado.app.constant.AppPattern.archiveFileRegex +import io.legado.app.utils.compress.LibArchiveUtils +import splitties.init.appCtx +import java.io.File + +/* 自动判断压缩文件后缀 然后再调用具体的实现 */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ArchiveUtils { + + const val TEMP_FOLDER_NAME = "ArchiveTemp" + + // 临时目录 下次启动自动删除 + val TEMP_PATH: String by lazy { + appCtx.externalCache.getFile(TEMP_FOLDER_NAME).createFolderReplace().absolutePath + } + + fun deCompress( + archiveUri: Uri, + path: String = TEMP_PATH, + filter: ((String) -> Boolean)? = null + ): List { + return deCompress(FileDoc.fromUri(archiveUri, false), path, filter) + } + + fun deCompress( + archivePath: String, + path: String = TEMP_PATH, + filter: ((String) -> Boolean)? = null + ): List { + return deCompress(Uri.parse(archivePath), path, filter) + } + + fun deCompress( + archiveFile: File, + path: String = TEMP_PATH, + filter: ((String) -> Boolean)? = null + ): List { + return deCompress(FileDoc.fromFile(archiveFile), path, filter) + } + + fun deCompress( + archiveDoc: DocumentFile, + path: String = TEMP_PATH, + filter: ((String) -> Boolean)? = null + ): List { + return deCompress(FileDoc.fromDocumentFile(archiveDoc), path, filter) + } + + fun deCompress( + archiveFileDoc: FileDoc, + path: String = TEMP_PATH, + filter: ((String) -> Boolean)? = null + ): List { + if (archiveFileDoc.isDir) throw IllegalArgumentException("Unexpected Folder input") + val name = archiveFileDoc.name + checkAchieve(name) + val workPathFileDoc = getCacheFolderFileDoc(name, path) + val workPath = workPathFileDoc.toString() + + return archiveFileDoc.openReadPfd().getOrThrow().use { + LibArchiveUtils.unArchive(it, File(workPath), filter) + } + + } + + /* 遍历目录获取文件名 */ + fun getArchiveFilesName(fileUri: Uri, filter: ((String) -> Boolean)? = null): List = + getArchiveFilesName(FileDoc.fromUri(fileUri, false), filter) + + + fun getArchiveFilesName( + fileDoc: FileDoc, + filter: ((String) -> Boolean)? = null + ): List { + val name = fileDoc.name + checkAchieve(name) + + return fileDoc.openReadPfd().getOrThrow().use { + try { + LibArchiveUtils.getFilesName(it, filter) + } catch (e: Exception) { + emptyList() + } + + } + + + } + + fun isArchive(name: String): Boolean { + return archiveFileRegex.matches(name) + } + + private fun checkAchieve(name: String) { + if (!isArchive(name)) + throw IllegalArgumentException("Unexpected file suffix: Only 7z rar zip Accepted") + } + + private fun getCacheFolderFileDoc( + archiveName: String, + workPath: String + ): FileDoc { + return FileDoc.fromUri(Uri.parse(workPath), true) + .createFolderIfNotExist(MD5Utils.md5Encode16(archiveName)) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/AsyncFileHandler.kt b/app/src/main/java/io/legado/app/utils/AsyncFileHandler.kt new file mode 100755 index 000000000..746afc08a --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/AsyncFileHandler.kt @@ -0,0 +1,18 @@ +package io.legado.app.utils + +import io.legado.app.help.globalExecutor +import java.util.logging.FileHandler +import java.util.logging.LogRecord + +class AsyncFileHandler(pattern: String) : FileHandler(pattern) { + + override fun publish(record: LogRecord?) { + if (!isLoggable(record)) { + return + } + globalExecutor.execute { + super.publish(record) + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt new file mode 100755 index 000000000..80c19e88f --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt @@ -0,0 +1,270 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Bitmap.Config +import android.graphics.BitmapFactory +import android.graphics.Color +import com.google.android.renderscript.Toolkit +import java.io.* +import kotlin.math.* + + +@Suppress("WeakerAccess", "MemberVisibilityCanBePrivate") +object BitmapUtils { + + /** + * 从path中获取图片信息,在通过BitmapFactory.decodeFile(String path)方法将突破转成Bitmap时, + * 遇到大一些的图片,我们经常会遇到OOM(Out Of Memory)的问题。所以用到了我们上面提到的BitmapFactory.Options这个类。 + * + * @param path 文件路径 + * @param width 想要显示的图片的宽度 + * @param height 想要显示的图片的高度 + * @return + */ + @Throws(IOException::class) + fun decodeBitmap(path: String, width: Int, height: Int? = null): Bitmap? { + val fis = FileInputStream(path) + return fis.use { + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeFileDescriptor(fis.fd, null, op) + op.inSampleSize = calculateInSampleSize(op, width, height) + op.inJustDecodeBounds = false + BitmapFactory.decodeFileDescriptor(fis.fd, null, op) + } + } + + /** + *计算 InSampleSize。缺省返回1 + * @param options BitmapFactory.Options, + * @param width 想要显示的图片的宽度 + * @param height 想要显示的图片的高度 + * @return + */ + private fun calculateInSampleSize( + options: BitmapFactory.Options, + width: Int? = null, + height: Int? = null + ): Int { + //获取比例大小 + val wRatio = width?.let { options.outWidth / it } ?: -1 + val hRatio = height?.let { options.outHeight / it } ?: -1 + //如果超出指定大小,则缩小相应的比例 + return when { + wRatio > 1 && hRatio > 1 -> max(wRatio, hRatio) + wRatio > 1 -> wRatio + hRatio > 1 -> hRatio + else -> 1 + } + } + + /** 从path中获取Bitmap图片 + * @param path 图片路径 + * @return + */ + @Throws(IOException::class) + fun decodeBitmap(path: String): Bitmap? { + val fis = FileInputStream(path) + return fis.use { + val opts = BitmapFactory.Options() + opts.inJustDecodeBounds = true + + BitmapFactory.decodeFileDescriptor(fis.fd, null, opts) + opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128) + opts.inJustDecodeBounds = false + BitmapFactory.decodeFileDescriptor(fis.fd, null, opts) + } + } + + /** + * 以最省内存的方式读取本地资源的图片 + * @param context 设备上下文 + * @param resId 资源ID + * @return + */ + fun decodeBitmap(context: Context, resId: Int): Bitmap? { + val opt = BitmapFactory.Options() + opt.inPreferredConfig = Config.RGB_565 + return BitmapFactory.decodeResource(context.resources, resId, opt) + } + + /** + * @param context 设备上下文 + * @param resId 资源ID + * @param width + * @param height + * @return + */ + fun decodeBitmap(context: Context, resId: Int, width: Int, height: Int): Bitmap? { + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeResource(context.resources, resId, op) //获取尺寸信息 + op.inSampleSize = calculateInSampleSize(op, width, height) + op.inJustDecodeBounds = false + return BitmapFactory.decodeResource(context.resources, resId, op) + } + + /** + * @param context 设备上下文 + * @param fileNameInAssets Assets里面文件的名称 + * @param width 图片的宽度 + * @param height 图片的高度 + * @return Bitmap + * @throws IOException + */ + @Throws(IOException::class) + fun decodeAssetsBitmap( + context: Context, + fileNameInAssets: String, + width: Int, + height: Int + ): Bitmap? { + var inputStream = context.assets.open(fileNameInAssets) + return inputStream.use { + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeStream(inputStream, null, op) //获取尺寸信息 + op.inSampleSize = calculateInSampleSize(op, width, height) + inputStream = context.assets.open(fileNameInAssets) + op.inJustDecodeBounds = false + BitmapFactory.decodeStream(inputStream, null, op) + } + } + + /** + * @param options + * @param minSideLength + * @param maxNumOfPixels + * @return + * 设置恰当的inSampleSize是解决该问题的关键之一。BitmapFactory.Options提供了另一个成员inJustDecodeBounds。 + * 设置inJustDecodeBounds为true后,decodeFile并不分配空间,但可计算出原始图片的长度和宽度,即opts.width和opts.height。 + * 有了这两个参数,再通过一定的算法,即可得到一个恰当的inSampleSize。 + * 查看Android源码,Android提供了下面这种动态计算的方法。 + */ + fun computeSampleSize( + options: BitmapFactory.Options, + minSideLength: Int, + maxNumOfPixels: Int + ): Int { + val initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels) + var roundedSize: Int + if (initialSize <= 8) { + roundedSize = 1 + while (roundedSize < initialSize) { + roundedSize = roundedSize shl 1 + } + } else { + roundedSize = (initialSize + 7) / 8 * 8 + } + return roundedSize + } + + + private fun computeInitialSampleSize( + options: BitmapFactory.Options, + minSideLength: Int, + maxNumOfPixels: Int + ): Int { + + val w = options.outWidth.toDouble() + val h = options.outHeight.toDouble() + + val lowerBound = when (maxNumOfPixels) { + -1 -> 1 + else -> ceil(sqrt(w * h / maxNumOfPixels)).toInt() + } + + val upperBound = when (minSideLength) { + -1 -> 128 + else -> min( + floor(w / minSideLength), + floor(h / minSideLength) + ).toInt() + } + + if (upperBound < lowerBound) { + // return the larger one when there is no overlapping zone. + return lowerBound + } + + return when { + maxNumOfPixels == -1 && minSideLength == -1 -> { + 1 + } + minSideLength == -1 -> { + lowerBound + } + else -> { + upperBound + } + } + } + + /** + * 将Bitmap转换成InputStream + * + * @param bitmap + * @return + */ + fun toInputStream(bitmap: Bitmap): InputStream { + val bos = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 90 /*ignored for PNG*/, bos) + return ByteArrayInputStream(bos.toByteArray()).also { bos.close() } + } + +} + +/** + * 获取指定宽高的图片 + */ +fun Bitmap.resizeAndRecycle(newWidth: Int, newHeight: Int): Bitmap { + //获取新的bitmap + val bitmap = Toolkit.resize(this, newWidth, newHeight) + recycle() + return bitmap +} + +/** + * 高斯模糊 + */ +fun Bitmap.stackBlur(radius: Int = 8): Bitmap { + return Toolkit.blur(this, radius) +} + +/** + * 取平均色 + */ +fun Bitmap.getMeanColor(): Int { + val width: Int = this.width + val height: Int = this.height + var pixel: Int + var pixelSumRed = 0 + var pixelSumBlue = 0 + var pixelSumGreen = 0 + for (i in 0..99) { + for (j in 70..99) { + pixel = this.getPixel( + (i * width / 100.toFloat()).roundToInt(), + (j * height / 100.toFloat()).roundToInt() + ) + pixelSumRed += Color.red(pixel) + pixelSumGreen += Color.green(pixel) + pixelSumBlue += Color.blue(pixel) + } + } + val averagePixelRed = pixelSumRed / 3000 + val averagePixelBlue = pixelSumBlue / 3000 + val averagePixelGreen = pixelSumGreen / 3000 + return Color.rgb( + averagePixelRed + 3, + averagePixelGreen + 3, + averagePixelBlue + 3 + ) + +} diff --git a/app/src/main/java/io/legado/app/utils/BookChapterExtensions.kt b/app/src/main/java/io/legado/app/utils/BookChapterExtensions.kt new file mode 100755 index 000000000..890062f6a --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/BookChapterExtensions.kt @@ -0,0 +1,8 @@ +package io.legado.app.utils + +import io.legado.app.data.entities.BookChapter + +fun BookChapter.internString() { + title = title.intern() + bookUrl = bookUrl.intern() +} diff --git a/app/src/main/java/io/legado/app/utils/ByteArrayExtensions.kt b/app/src/main/java/io/legado/app/utils/ByteArrayExtensions.kt new file mode 100755 index 000000000..3ab51443d --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ByteArrayExtensions.kt @@ -0,0 +1,45 @@ +package io.legado.app.utils + + +/** + * Search the data byte array for the first occurrence + * of the byte array pattern. + */ +fun ByteArray.indexOf(pattern: ByteArray, start: Int = 0, stop: Int = size): Int { + val data = this + val failure: IntArray = computeFailure(pattern) + + var j = 0 + + for (i in start until stop) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1] + } + if (pattern[j] == data[i]) { + j++ + } + if (j == pattern.size) { + return i - pattern.size + 1 + } + } + return -1 +} + +/** + * Computes the failure function using a boot-strapping process, + * where the pattern is matched against itself. + */ +private fun computeFailure(pattern: ByteArray): IntArray { + val failure = IntArray(pattern.size) + var j = 0 + for (i in 1 until pattern.size) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1] + } + if (pattern[j] == pattern[i]) { + j++ + } + failure[i] = j + } + return failure +} diff --git a/app/src/main/java/io/legado/app/utils/ChineseUtils.kt b/app/src/main/java/io/legado/app/utils/ChineseUtils.kt new file mode 100755 index 000000000..a363f47d6 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ChineseUtils.kt @@ -0,0 +1,51 @@ +package io.legado.app.utils + +import com.github.liuyueyi.quick.transfer.ChineseUtils +import com.github.liuyueyi.quick.transfer.constants.TransType + +object ChineseUtils { + + private var fixed = false + + fun s2t(content: String): String { + return ChineseUtils.s2t(content) + } + + fun t2s(content: String): String { + if (!fixed) { + fixT2sDict() + } + return ChineseUtils.t2s(content) + } + + fun preLoad(async: Boolean, vararg transType: TransType) { + ChineseUtils.preLoad(async, *transType) + } + + fun unLoad(vararg transType: TransType) { + ChineseUtils.unLoad(*transType) + } + + fun fixT2sDict() { + fixed = true + val excludeList = listOf( + "槃", + "划槳", "列根", "雪梨", "雪糕", "多士", "起司", "芝士", "沙芬", "母音", + "华乐", "民乐", "晶元", "晶片", "映像", "明覆", "明瞭", "新力", "新喻", + "零錢", "零钱", "離線", "碟片", "模組", "桌球", "案頭", "機車", "電漿", + "鳳梨", "魔戒", "載入", "菲林", "整合", "變數", "解碼", "散钱", "插水", + "房屋", "房价", "快取", "德士", "建立", "常式", "席丹", "布殊", "布希", + "巴哈", "巨集", "夜学", "向量", "半形", "加彭", "列印", "函式", "全形", + "光碟", "介面", "乳酪", "沈船", "永珍", "演化", "牛油", "相容", "磁碟", + "菲林", "規則", "酵素", "雷根", "饭盒", + "路易斯", "非同步", "出租车", "周杰倫", "马铃薯", "馬鈴薯", "機械人", "電單車", + "電扶梯", "音效卡", "飆車族", "點陣圖", "個入球", "顆進球", "沃尓沃", "晶片集", + "斯瓦巴", "斜角巷", "战列舰", "快速面", "希特拉", "太空梭", "吐瓦魯", "吉布堤", + "吉布地", "史太林", "南冰洋", "区域网", "波札那", "解析度", "酷洛米", "金夏沙", + "魔獸紀元", "高空彈跳", "铁达尼号", "太空战士", "埃及妖后", "吉里巴斯", "附加元件", + "魔鬼終結者", "純文字檔案", "奇幻魔法Melody", "列支敦斯登" + ) + ChineseUtils.loadExcludeDict(TransType.TRADITIONAL_TO_SIMPLE, excludeList) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt new file mode 100755 index 000000000..f64df9239 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/CollectionExtensions.kt @@ -0,0 +1,60 @@ +package io.legado.app.utils + +fun List.fastSum(): Float { + var sum = 0f + for (i in indices) { + sum += this[i] + } + return sum +} + +inline fun List.fastBinarySearch( + fromIndex: Int = 0, + toIndex: Int = size, + comparison: (T) -> Int +): Int { + when { + fromIndex > toIndex -> + throw IllegalArgumentException( + "fromIndex ($fromIndex) is greater than toIndex ($toIndex)." + ) + + fromIndex < 0 -> + throw IndexOutOfBoundsException("fromIndex ($fromIndex) is less than zero.") + + toIndex > size -> + throw IndexOutOfBoundsException("toIndex ($toIndex) is greater than size ($size).") + } + + var low = fromIndex + var high = toIndex - 1 + + while (low <= high) { + val mid = (low + high).ushr(1) // safe from overflows + val midVal = get(mid) + val cmp = comparison(midVal) + + if (cmp < 0) + low = mid + 1 + else if (cmp > 0) + high = mid - 1 + else + return mid // key found + } + return -(low + 1) // key not found +} + +inline fun > List.fastBinarySearchBy( + key: K?, + fromIndex: Int = 0, + toIndex: Int = size, + crossinline selector: (T) -> K? +): Int = fastBinarySearch(fromIndex, toIndex) { compareValues(selector(it), key) } + +fun MutableList.removeLastElement(): T { + return if (isEmpty()) { + throw NoSuchElementException("List is empty.") + } else { + removeAt(lastIndex) + } +} diff --git a/app/src/main/java/io/legado/app/utils/ColorUtils.kt b/app/src/main/java/io/legado/app/utils/ColorUtils.kt new file mode 100755 index 000000000..02d6ba8e4 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ColorUtils.kt @@ -0,0 +1,119 @@ +package io.legado.app.utils + +import android.graphics.Color +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import androidx.core.graphics.ColorUtils +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sqrt + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ColorUtils { + + fun isColorLight(@ColorInt color: Int): Boolean { + return ColorUtils.calculateLuminance(color) >= 0.5 + } + + fun intToString(intColor: Int): String { + return String.format("#%06X", 0xFFFFFF and intColor) + } + + fun stripAlpha(@ColorInt color: Int): Int { + return -0x1000000 or color + } + + @ColorInt + fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int { + if (by == 1f) return color + val alpha = Color.alpha(color) + val hsv = FloatArray(3) + Color.colorToHSV(color, hsv) + hsv[2] *= by // value component + return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv)) + } + + @ColorInt + fun darkenColor(@ColorInt color: Int): Int { + return shiftColor(color, 0.9f) + } + + @ColorInt + fun lightenColor(@ColorInt color: Int): Int { + return shiftColor(color, 1.1f) + } + + @ColorInt + fun invertColor(@ColorInt color: Int): Int { + val r = 255 - Color.red(color) + val g = 255 - Color.green(color) + val b = 255 - Color.blue(color) + return Color.argb(Color.alpha(color), r, g, b) + } + + @ColorInt + fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int { + val alpha = (Color.alpha(color) * factor).roundToInt() + val red = Color.red(color) + val green = Color.green(color) + val blue = Color.blue(color) + return Color.argb(alpha, red, green, blue) + } + + @ColorInt + fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int { + val a = min(255, max(0, (alpha * 255).toInt())) shl 24 + val rgb = 0x00ffffff and baseColor + return a + rgb + } + + /** + * Taken from CollapsingToolbarLayout's CollapsingTextHelper class. + */ + fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int { + val inverseRatio = 1f - ratio + val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio + val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio + val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio + val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio + return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt()) + } + + fun argb(r: Int, g: Int, b: Int): Int { + return argb(Byte.MAX_VALUE.toInt(), r, g, b) + } + + fun argb(alpha: Int, r: Int, g: Int, b: Int): Int { + val colorByteArr = + byteArrayOf(alpha.toByte(), r.toByte(), g.toByte(), b.toByte()) + return byteArrToInt(colorByteArr) + } + + fun rgb(argb: Int): IntArray { + return intArrayOf(argb shr 16 and 0xFF, argb shr 8 and 0xFF, argb and 0xFF) + } + + fun byteArrToInt(colorByteArr: ByteArray): Int { + return ((colorByteArr[0].toInt() shl 24) + (colorByteArr[1].toInt() and 0xFF shl 16) + + (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF)) + } + + /** + * Computes the difference between two RGB colors by converting them to the L*a*b scale and + * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76} + */ + fun getColorDifference(a: Int, b: Int): Double { + val lab1 = DoubleArray(3) + val lab2 = DoubleArray(3) + ColorUtils.colorToLAB(a, lab1) + ColorUtils.colorToLAB(b, lab2) + return sqrt( + (lab2[0] - lab1[0]) + .pow(2.0) + (lab2[1] - lab1[1]) + .pow(2.0) + (lab2[2] - lab1[2]) + .pow(2.0) + ) + } +} diff --git a/app/src/main/java/io/legado/app/utils/ConfigurationExtensions.kt b/app/src/main/java/io/legado/app/utils/ConfigurationExtensions.kt new file mode 100755 index 000000000..186e8ae69 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ConfigurationExtensions.kt @@ -0,0 +1,14 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.content.res.Configuration +import android.content.res.Resources + +val sysConfiguration: Configuration = Resources.getSystem().configuration + +val Configuration.isNightMode: Boolean + get() { + val mode = uiMode and Configuration.UI_MODE_NIGHT_MASK + return mode == Configuration.UI_MODE_NIGHT_YES + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ConflateLiveData.kt b/app/src/main/java/io/legado/app/utils/ConflateLiveData.kt new file mode 100755 index 000000000..6e30ba16e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ConflateLiveData.kt @@ -0,0 +1,37 @@ +package io.legado.app.utils + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LiveData + +/** + * 合并发送,只发送最新数据 + * @param delay 发送时间间隔 + */ +class ConflateLiveData(val delay: Int) : LiveData() { + private val handler = Handler(Looper.getMainLooper()) + private val sendRunnable = Runnable { sendData() } + private var postTime = 0L + private var data: T? = null + + private fun sendData() { + data?.let { + super.postValue(it) + } + } + + @Synchronized + public override fun postValue(value: T) { + data = value + val postDelay = postTime + delay - System.currentTimeMillis() + if (postDelay > 0) { + handler.removeCallbacks(sendRunnable) + handler.postDelayed(sendRunnable, postDelay) + } else { + handler.removeCallbacks(sendRunnable) + postTime = System.currentTimeMillis() + super.postValue(value) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ConstraintModify.kt b/app/src/main/java/io/legado/app/utils/ConstraintModify.kt new file mode 100755 index 000000000..9b092a809 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ConstraintModify.kt @@ -0,0 +1,298 @@ +package io.legado.app.utils + +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.transition.TransitionManager + +@Suppress("unused") +fun ConstraintLayout.modifyBegin(withAnim: Boolean = false): ConstraintModify.ConstraintBegin { + val begin = ConstraintModify(this).begin + if (withAnim) { + TransitionManager.beginDelayedTransition(this) + } + return begin +} + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ConstraintModify(private val constraintLayout: ConstraintLayout) { + + val begin: ConstraintBegin by lazy { + applyConstraintSet.clone(constraintLayout) + ConstraintBegin(constraintLayout, applyConstraintSet) + } + private val applyConstraintSet = ConstraintSet() + private val resetConstraintSet = ConstraintSet() + + init { + resetConstraintSet.clone(constraintLayout) + } + + /** + * 带动画的修改 + * @return + */ + fun beginWithAnim(): ConstraintBegin { + TransitionManager.beginDelayedTransition(constraintLayout) + return begin + } + + /** + * 重置 + */ + fun reSet() { + resetConstraintSet.applyTo(constraintLayout) + } + + /** + * 带动画的重置 + */ + fun reSetWidthAnim() { + TransitionManager.beginDelayedTransition(constraintLayout) + resetConstraintSet.applyTo(constraintLayout) + } + + + @Suppress("unused", "MemberVisibilityCanBePrivate") + class ConstraintBegin( + private val constraintLayout: ConstraintLayout, + private val applyConstraintSet: ConstraintSet + ) { + + /** + * 清除关系,这里不仅仅会清除关系,还会清除对应控件的宽高为 w:0,h:0 + * @param viewId 视图ID + * @return + */ + fun clear(viewId: Int): ConstraintBegin { + applyConstraintSet.clear(viewId) + return this + } + + /** + * 清除某个控件的,某个关系 + * @param viewId 控件ID + * @param anchor 要解除的关系 + * @return + */ + fun clear(viewId: Int, anchor: Anchor): ConstraintBegin { + applyConstraintSet.clear(viewId, anchor.toInt()) + return this + } + + fun setHorizontalWeight(viewId: Int, weight: Float): ConstraintBegin { + applyConstraintSet.setHorizontalWeight(viewId, weight) + return this + } + + fun setVerticalWeight(viewId: Int, weight: Float): ConstraintBegin { + applyConstraintSet.setVerticalWeight(viewId, weight) + return this + } + + /** + * 为某个控件设置 margin + * @param viewId 某个控件ID + * @param left marginLeft + * @param top marginTop + * @param right marginRight + * @param bottom marginBottom + * @return + */ + fun setMargin( + @IdRes viewId: Int, + left: Int, + top: Int, + right: Int, + bottom: Int + ): ConstraintBegin { + setMarginLeft(viewId, left) + setMarginTop(viewId, top) + setMarginRight(viewId, right) + setMarginBottom(viewId, bottom) + return this + } + + /** + * 为某个控件设置 marginLeft + * @param viewId 某个控件ID + * @param left marginLeft + * @return + */ + fun setMarginLeft(@IdRes viewId: Int, left: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.LEFT, left) + return this + } + + /** + * 为某个控件设置 marginRight + * @param viewId 某个控件ID + * @param right marginRight + * @return + */ + fun setMarginRight(@IdRes viewId: Int, right: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.RIGHT, right) + return this + } + + /** + * 为某个控件设置 marginTop + * @param viewId 某个控件ID + * @param top marginTop + * @return + */ + fun setMarginTop(@IdRes viewId: Int, top: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.TOP, top) + return this + } + + /** + * 为某个控件设置marginBottom + * @param viewId 某个控件ID + * @param bottom marginBottom + * @return + */ + fun setMarginBottom(@IdRes viewId: Int, bottom: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.BOTTOM, bottom) + return this + } + + /** + * 为某个控件设置关联关系 left_to_left_of + * @param startId + * @param endId + * @return + */ + fun leftToLeftOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.LEFT, endId, ConstraintSet.LEFT) + return this + } + + /** + * 为某个控件设置关联关系 left_to_right_of + * @param startId + * @param endId + * @return + */ + fun leftToRightOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.LEFT, endId, ConstraintSet.RIGHT) + return this + } + + /** + * 为某个控件设置关联关系 top_to_top_of + * @param startId + * @param endId + * @return + */ + fun topToTopOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.TOP, endId, ConstraintSet.TOP) + return this + } + + /** + * 为某个控件设置关联关系 top_to_bottom_of + * @param startId + * @param endId + * @return + */ + fun topToBottomOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.TOP, endId, ConstraintSet.BOTTOM) + return this + } + + /** + * 为某个控件设置关联关系 right_to_left_of + * @param startId + * @param endId + * @return + */ + fun rightToLeftOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.RIGHT, endId, ConstraintSet.LEFT) + return this + } + + /** + * 为某个控件设置关联关系 right_to_right_of + * @param startId + * @param endId + * @return + */ + fun rightToRightOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.RIGHT, endId, ConstraintSet.RIGHT) + return this + } + + /** + * 为某个控件设置关联关系 bottom_to_bottom_of + * @param startId + * @param endId + * @return + */ + fun bottomToBottomOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.BOTTOM) + return this + } + + /** + * 为某个控件设置关联关系 bottom_to_top_of + * @param startId + * @param endId + * @return + */ + fun bottomToTopOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.TOP) + return this + } + + /** + * 为某个控件设置宽度 + * @param viewId + * @param width + * @return + */ + fun setWidth(@IdRes viewId: Int, width: Int): ConstraintBegin { + applyConstraintSet.constrainWidth(viewId, width) + return this + } + + /** + * 某个控件设置高度 + * @param viewId + * @param height + * @return + */ + fun setHeight(@IdRes viewId: Int, height: Int): ConstraintBegin { + applyConstraintSet.constrainHeight(viewId, height) + return this + } + + /** + * 提交应用生效 + */ + fun commit() { + constraintLayout.post { + applyConstraintSet.applyTo(constraintLayout) + } + } + } + + enum class Anchor { + LEFT, RIGHT, TOP, BOTTOM, BASELINE, START, END, CIRCLE_REFERENCE; + + fun toInt(): Int { + return when (this) { + LEFT -> ConstraintSet.LEFT + RIGHT -> ConstraintSet.RIGHT + TOP -> ConstraintSet.TOP + BOTTOM -> ConstraintSet.BOTTOM + BASELINE -> ConstraintSet.BASELINE + START -> ConstraintSet.START + END -> ConstraintSet.END + CIRCLE_REFERENCE -> ConstraintSet.CIRCLE_REFERENCE + } + } + + } + +} diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt new file mode 100755 index 000000000..074cba333 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -0,0 +1,417 @@ +@file:Suppress("unused", "UnusedReceiverParameter") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_MUTABLE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.PendingIntent.getActivity +import android.app.PendingIntent.getBroadcast +import android.app.PendingIntent.getService +import android.app.Service +import android.content.BroadcastReceiver +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.ConnectivityManager +import android.net.Uri +import android.os.BatteryManager +import android.os.Build +import android.os.Process +import android.provider.Settings +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import io.legado.app.R +import io.legado.app.constant.AppConst +import io.legado.app.data.entities.Book +import io.legado.app.help.IntentHelp +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isImage +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.ui.book.manga.ReadMangaActivity +import io.legado.app.ui.book.read.ReadBookActivity +import splitties.systemservices.clipboardManager +import splitties.systemservices.connectivityManager +import splitties.systemservices.uiModeManager +import java.io.File +import java.io.FileOutputStream +import kotlin.system.exitProcess + +inline fun Context.startActivity(configIntent: Intent.() -> Unit = {}) { + val intent = Intent(this, A::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.apply(configIntent) + startActivity(intent) +} + +fun Context.startActivityForBook( + book: Book, + configIntent: Intent.() -> Unit = {}, +) { + val cls = when { + book.isAudio -> AudioPlayActivity::class.java + !book.isLocal && book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java + else -> ReadBookActivity::class.java + } + val intent = Intent(this, cls) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra("bookUrl", book.bookUrl) + intent.apply(configIntent) + startActivity(intent) +} + + +inline fun Context.startService(configIntent: Intent.() -> Unit = {}) { + startService(Intent(this, T::class.java).apply(configIntent)) +} + +inline fun Context.stopService() { + stopService(Intent(this, T::class.java)) +} + +@SuppressLint("UnspecifiedImmutableFlag") +inline fun Context.servicePendingIntent( + action: String, + requestCode: Int = 0, + configIntent: Intent.() -> Unit = {}, +): PendingIntent? { + val intent = Intent(this, T::class.java) + intent.action = action + configIntent.invoke(intent) + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + FLAG_UPDATE_CURRENT or FLAG_MUTABLE + } else { + FLAG_UPDATE_CURRENT + } + return getService(this, requestCode, intent, flags) +} + +@SuppressLint("UnspecifiedImmutableFlag") +fun Context.activityPendingIntent( + intent: Intent, + action: String, +): PendingIntent? { + intent.action = action + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + FLAG_UPDATE_CURRENT or FLAG_MUTABLE + } else { + FLAG_UPDATE_CURRENT + } + return getActivity(this, 0, intent, flags) +} + +@SuppressLint("UnspecifiedImmutableFlag") +inline fun Context.activityPendingIntent( + action: String, + configIntent: Intent.() -> Unit = {}, +): PendingIntent? { + val intent = Intent(this, T::class.java) + intent.action = action + configIntent.invoke(intent) + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + FLAG_UPDATE_CURRENT or FLAG_MUTABLE + } else { + FLAG_UPDATE_CURRENT + } + return getActivity(this, 0, intent, flags) +} + +@SuppressLint("UnspecifiedImmutableFlag") +inline fun Context.broadcastPendingIntent( + action: String, + configIntent: Intent.() -> Unit = {}, +): PendingIntent? { + val intent = Intent(this, T::class.java) + intent.action = action + configIntent.invoke(intent) + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + FLAG_UPDATE_CURRENT or FLAG_MUTABLE + } else { + FLAG_UPDATE_CURRENT + } + return getBroadcast(this, 0, intent, flags) +} + +fun Context.startForegroundServiceCompat(intent: Intent) { + try { + startService(intent) + } catch (e: IllegalStateException) { + ContextCompat.startForegroundService(this, intent) + } +} + +val Context.defaultSharedPreferences: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(this) + +fun Context.getPrefBoolean(key: String, defValue: Boolean = false) = + defaultSharedPreferences.getBoolean(key, defValue) + +fun Context.putPrefBoolean(key: String, value: Boolean = false) = + defaultSharedPreferences.edit { putBoolean(key, value) } + +fun Context.getPrefInt(key: String, defValue: Int = 0) = + defaultSharedPreferences.getInt(key, defValue) + +fun Context.putPrefInt(key: String, value: Int) = + defaultSharedPreferences.edit { putInt(key, value) } + +fun Context.getPrefLong(key: String, defValue: Long = 0L) = + defaultSharedPreferences.getLong(key, defValue) + +fun Context.putPrefLong(key: String, value: Long) = + defaultSharedPreferences.edit { putLong(key, value) } + +fun Context.getPrefString(key: String, defValue: String? = null) = + defaultSharedPreferences.getString(key, defValue) + +fun Context.putPrefString(key: String, value: String?) = + defaultSharedPreferences.edit { putString(key, value) } + +fun Context.getPrefStringSet( + key: String, + defValue: MutableSet? = null, +): MutableSet? = defaultSharedPreferences.getStringSet(key, defValue) + +fun Context.putPrefStringSet(key: String, value: MutableSet) = + defaultSharedPreferences.edit { putStringSet(key, value) } + +fun Context.removePref(key: String) = + defaultSharedPreferences.edit { remove(key) } + + +fun Context.getCompatColor(@ColorRes id: Int): Int = ContextCompat.getColor(this, id) + +fun Context.getCompatDrawable(@DrawableRes id: Int): Drawable? = ContextCompat.getDrawable(this, id) + +fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = + ContextCompat.getColorStateList(this, id) + +fun Context.checkSelfUriPermission(uri: Uri, modeFlags: Int): Int = + checkUriPermission(uri, Process.myPid(), Process.myUid(), modeFlags) + +fun Context.restart() { + val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName) + intent?.let { + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_CLEAR_TASK + or Intent.FLAG_ACTIVITY_CLEAR_TOP + ) + startActivity(intent) + //杀掉以前进程 + Process.killProcess(Process.myPid()) + exitProcess(0) + } +} + +/** + * 系统息屏时间 + */ +val Context.sysScreenOffTime: Int + get() { + return kotlin.runCatching { + Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT) + }.onFailure { + it.printOnDebug() + }.getOrDefault(0) + } + +val Context.statusBarHeight: Int + @SuppressLint("DiscouragedApi", "InternalInsetResource") + get() { + if (Build.BOARD == "windows") { + return 0 + } + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + +val Context.navigationBarHeight: Int + @SuppressLint("DiscouragedApi", "InternalInsetResource") + get() { + val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") + return resources.getDimensionPixelSize(resourceId) + } + +fun Context.share(text: String, title: String = getString(R.string.share)) { + kotlin.runCatching { + val intent = Intent(Intent.ACTION_SEND) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(Intent.EXTRA_SUBJECT, title) + intent.putExtra(Intent.EXTRA_TEXT, text) + intent.type = "text/plain" + startActivity(Intent.createChooser(intent, title)) + } +} + +fun Context.share(file: File, type: String = "text/*") { + val fileUri = FileProvider.getUriForFile(this, AppConst.authority, file) + val intent = Intent(Intent.ACTION_SEND) + intent.type = type + intent.putExtra(Intent.EXTRA_STREAM, fileUri) + intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity( + Intent.createChooser( + intent, + getString(R.string.share_selected_source) + ) + ) +} + +@SuppressLint("SetWorldReadable") +fun Context.shareWithQr( + text: String, + title: String = getString(R.string.share), + errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H, +) { + val bitmap = QRCodeUtils.createQRCode(text, errorCorrectionLevel = errorCorrectionLevel) + if (bitmap == null) { + toastOnUi(R.string.text_too_long_qr_error) + } else { + try { + val file = File(externalCacheDir, "qr.png") + val fOut = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut) + fOut.flush() + fOut.close() + file.setReadable(true, false) + val contentUri = FileProvider.getUriForFile(this, AppConst.authority, file) + val intent = Intent(Intent.ACTION_SEND) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra(Intent.EXTRA_STREAM, contentUri) + intent.type = "image/png" + startActivity(Intent.createChooser(intent, title)) + } catch (e: Exception) { + toastOnUi(e.localizedMessage ?: "ERROR") + } + } +} + +fun Context.sendToClip(text: String) { + val clipData = ClipData.newPlainText(null, text) + clipboardManager.setPrimaryClip(clipData) + longToastOnUi(R.string.copy_complete) +} + +fun Context.getClipText(): String? { + clipboardManager.primaryClip?.let { + if (it.itemCount > 0) { + return it.getItemAt(0).text.toString().trim() + } + } + return null +} + +fun Context.sendMail(mail: String) { + try { + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:$mail") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } catch (e: Exception) { + toastOnUi(e.localizedMessage ?: "Error") + } +} + +/** + * 获取电量 + */ +val Context.sysBattery: Int + get() { + val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus = registerReceiver(null, iFilter) + return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 + } + +val Context.externalFiles: File + get() = this.getExternalFilesDir(null) ?: this.filesDir + +val Context.externalCache: File + get() = this.externalCacheDir ?: this.cacheDir + +fun Context.openUrl(url: String) { + try { + startActivity(IntentHelp.getBrowserIntent(url)) + } catch (e: Exception) { + toastOnUi(e.localizedMessage ?: "open url error") + e.printOnDebug() + } +} + +fun Context.openUrl(uri: Uri) { + try { + startActivity(IntentHelp.getBrowserIntent(uri)) + } catch (e: Exception) { + toastOnUi(e.localizedMessage ?: "open url error") + e.printOnDebug() + } +} + +@SuppressLint("ObsoleteSdkInt") +fun Context.openFileUri(uri: Uri, type: String? = null) { + val intent = Intent() + intent.action = Intent.ACTION_VIEW + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + //7.0版本以上 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val uri = if (uri.isContentScheme()) uri + else FileProvider.getUriForFile(this, AppConst.authority, File(uri.path!!)) + intent.setDataAndType(uri, type ?: IntentType.from(uri)) + try { + startActivity(intent) + } catch (e: Exception) { + toastOnUi(e.stackTraceStr) + e.printOnDebug() + } +} + +@Suppress("DEPRECATION") +val Context.isWifiConnect: Boolean + @SuppressLint("MissingPermission") + get() { + val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI) + return info?.isConnected == true + } + +val Context.isPad: Boolean + get() { + return (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE + } + +val Context.isTv: Boolean + get() = uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION + +val Context.channel: String + get() { + try { + val pm = packageManager + val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + return appInfo.metaData.getString("channel") ?: "" + } catch (e: Exception) { + e.printOnDebug() + } + return "" + } + +val Context.isDebuggable: Boolean + get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 diff --git a/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt b/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt new file mode 100755 index 000000000..167664327 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ConvertExtensions.kt @@ -0,0 +1,131 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import java.text.DecimalFormat +import kotlin.math.log10 +import kotlin.math.pow + +/** + * 数据类型转换、单位转换 + * + * @author 李玉江[QQ:1023694760] + * @since 2014-4-18 + */ +@Suppress("MemberVisibilityCanBePrivate") +object ConvertUtils { + const val GB: Long = 1073741824 + const val MB: Long = 1048576 + const val KB: Long = 1024 + + fun toInt(obj: Any): Int { + return kotlin.runCatching { + Integer.parseInt(obj.toString()) + }.getOrDefault(-1) + } + + fun toInt(bytes: ByteArray): Int { + var result = 0 + var byte: Byte + for (i in bytes.indices) { + byte = bytes[i] + result += (byte.toInt() and 0xFF).shl(8 * i) + } + return result + } + + fun toFloat(obj: Any): Float { + return kotlin.runCatching { + java.lang.Float.parseFloat(obj.toString()) + }.getOrDefault(-1f) + } + + fun toString(objects: Array, tag: String): String { + val sb = StringBuilder() + for (`object` in objects) { + sb.append(`object`) + sb.append(tag) + } + return sb.toString() + } + + @JvmOverloads + fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? { + var bitmap: Bitmap? = null + if (bytes.isNotEmpty()) { + kotlin.runCatching { + val options = BitmapFactory.Options() + // 设置让解码器以最佳方式解码 + options.inPreferredConfig = null + if (width > 0 && height > 0) { + options.outWidth = width + options.outHeight = height + } + bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) + bitmap!!.density = 96// 96 dpi + } + } + return bitmap + } + + private fun toDrawable(bitmap: Bitmap?): Drawable? { + return if (bitmap == null) null else BitmapDrawable(Resources.getSystem(), bitmap) + } + + fun toDrawable(bytes: ByteArray): Drawable? { + return toDrawable(toBitmap(bytes)) + } + + fun formatFileSize(length: Long): String { + if (length <= 0) return "0" + val units = arrayOf("b", "kb", "M", "G", "T") + //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 + val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt() + //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 + return DecimalFormat("#,##0.##").format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] + } + + @JvmOverloads + fun toString(`is`: InputStream, charset: String = "utf-8"): String { + val sb = StringBuilder() + kotlin.runCatching { + val reader = BufferedReader(InputStreamReader(`is`, charset)) + while (true) { + val line = reader.readLine() + if (line == null) { + break + } else { + sb.append(line).append("\n") + } + } + reader.close() + `is`.close() + } + return sb.toString() + } + + +} + +val Int.hexString: String + get() = Integer.toHexString(this) + +fun Int.dpToPx(): Int = this.toFloat().dpToPx().toInt() + +fun Int.spToPx(): Int = this.toFloat().spToPx().toInt() + +fun Float.dpToPx(): Float = android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics +) + +fun Float.spToPx(): Float = android.util.TypedValue.applyDimension( + android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/CookieManagerExtensions.kt b/app/src/main/java/io/legado/app/utils/CookieManagerExtensions.kt new file mode 100755 index 000000000..ba57149e6 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/CookieManagerExtensions.kt @@ -0,0 +1,22 @@ +@file:Suppress("UnusedReceiverParameter") + +package io.legado.app.utils + +import android.webkit.CookieManager + + +@Suppress("unused") +fun CookieManager.removeCookie(url: String) { + val cm = CookieManager.getInstance() + val domains = arrayOf( + NetworkUtils.getDomain(url), + NetworkUtils.getSubDomain(url) + ) + domains.forEach { dm -> + val cookieGlob: String? = cm.getCookie(dm) + cookieGlob?.splitNotBlank(";")?.forEach { + val cookieName = it.substringBefore("=") + cm.setCookie(dm, "$cookieName=; Expires=Wed, 31 Dec 2000 23:59:59 GMT") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/CoroutineExtensions.kt b/app/src/main/java/io/legado/app/utils/CoroutineExtensions.kt new file mode 100755 index 000000000..abe990bce --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/CoroutineExtensions.kt @@ -0,0 +1,37 @@ +package io.legado.app.utils + +import io.legado.app.help.coroutine.Coroutine +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class TimeoutCancellationException(msg: String) : CancellationException(msg) + +suspend fun withTimeoutAsync(delayMillis: Long, block: suspend CoroutineScope.() -> T): T { + return suspendCancellableCoroutine { cout -> + Coroutine.async(context = cout.context) { + launch { + delay(delayMillis) + if (!cout.isCompleted) { + cout.resumeWithException(TimeoutCancellationException("Timed out waiting for $delayMillis ms")) + } + } + val result = block() + if (!cout.isCompleted) { + cout.resume(result) + } + } + } +} + +suspend fun withTimeoutOrNullAsync(delayMillis: Long, block: suspend CoroutineScope.() -> T): T? { + return try { + withTimeoutAsync(delayMillis, block) + } catch (e: TimeoutCancellationException) { + null + } +} diff --git a/app/src/main/java/io/legado/app/utils/CustomExportUtils.kt b/app/src/main/java/io/legado/app/utils/CustomExportUtils.kt new file mode 100755 index 000000000..80fdf2c70 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/CustomExportUtils.kt @@ -0,0 +1,27 @@ +package io.legado.app.utils + +import io.legado.app.help.config.AppConfig + +// 匹配待“输入的章节”字符串 +private val regexEpisode = Regex("\\d+(-\\d+)?(,\\d+(-\\d+)?)*") + +/** + * 是否启用自定义导出 + * + * @author Discut + */ +fun enableCustomExport(): Boolean { + return AppConfig.enableCustomExport && AppConfig.exportType == 1 +} + +/** + * 验证 输入的范围 是否正确 + * + * @since 1.0.0 + * @author Discut + * @param text 输入的范围 字符串 + * @return 是否正确 + */ +fun verificationField(text: String): Boolean { + return text.matches(regexEpisode) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/Debounce.kt b/app/src/main/java/io/legado/app/utils/Debounce.kt new file mode 100755 index 000000000..ab1e4cc3f --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/Debounce.kt @@ -0,0 +1,129 @@ +package io.legado.app.utils + +import android.os.SystemClock +import kotlin.math.max + +@Suppress("MemberVisibilityCanBePrivate") +open class Debounce( + var wait: Long = 0L, + var maxWait: Long = -1L, + var leading: Boolean = false, + var trailing: Boolean = true, + private val func: () -> T +) { + companion object { + private val handler by lazy { buildMainHandler() } + } + + private var lastCallTime = -1L + private var lastInvokeTime = 0L + private val maxing get() = maxWait != -1L + private var result: T? = null + private var hasTimer = false + private val timerExpiredRunnable = Runnable { + timerExpired() + } + + init { + maxWait = if (maxing) max(maxWait, wait) else maxWait + } + + private fun invokeFunc(time: Long): T { + lastInvokeTime = time + return func.invoke().also { result = it } + } + + private fun startTimer(wait: Long) { + hasTimer = true + handler.postDelayed(timerExpiredRunnable, wait) + } + + private fun cancelTimer() { + handler.removeCallbacks(timerExpiredRunnable) + } + + private fun leadingEdge(time: Long): T? { + lastInvokeTime = time + startTimer(wait) + return if (leading) invokeFunc(time) else result + } + + private fun trailingEdge(time: Long): T? { + hasTimer = false + return if (trailing) invokeFunc(time) else result + } + + private fun remainingWait(time: Long): Long { + val timeSinceLastCall = time - lastCallTime + val timeSinceLastInvoke = time - lastInvokeTime + val timeWaiting = wait - timeSinceLastCall + + return if (maxing) timeWaiting.coerceAtMost(maxWait - timeSinceLastInvoke) else timeWaiting + } + + private fun shouldInvoke(time: Long): Boolean { + val timeSinceLastCall = time - lastCallTime + val timeSinceLastInvoke = time - lastInvokeTime + + return lastCallTime == -1L + || timeSinceLastCall >= wait + || timeSinceLastCall < 0 + || maxing && timeSinceLastInvoke >= maxWait + } + + private fun timerExpired() { + val time = SystemClock.uptimeMillis() + if (shouldInvoke(time)) { + trailingEdge(time) + } else { + startTimer(remainingWait(time)) + } + } + + fun cancel() { + if (hasTimer) { + cancelTimer() + } + lastInvokeTime = 0 + lastCallTime = -1L + hasTimer = false + } + + fun flush(): T? { + return if (hasTimer) trailingEdge(SystemClock.uptimeMillis()) else result + } + + fun pending(): Boolean = hasTimer + + operator fun invoke(): T? { + val time = SystemClock.uptimeMillis() + val isInvoking = shouldInvoke(time) + + lastCallTime = time + + if (isInvoking) { + if (!hasTimer) { + return leadingEdge(lastCallTime) + } + if (maxing) { + startTimer(wait) + return invokeFunc(lastCallTime) + } + } + + if (!hasTimer) { + startTimer(wait) + } + + return result + } + +} + +fun debounce( + wait: Long = 0L, + maxWait: Long = -1L, + leading: Boolean = false, + trailing: Boolean = true, + func: () -> T +) = Debounce(wait, maxWait, leading, trailing, func) diff --git a/app/src/main/java/io/legado/app/utils/DebugLog.kt b/app/src/main/java/io/legado/app/utils/DebugLog.kt new file mode 100755 index 000000000..07295b45f --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/DebugLog.kt @@ -0,0 +1,54 @@ +package io.legado.app.utils + +import android.util.Log +import io.legado.app.BuildConfig + +object DebugLog { + + fun e(tag: String, throwable: Throwable) { + if (BuildConfig.DEBUG) { + Log.e(tag, throwable.stackTraceToString()) + } + } + + fun e(tag: String, msg: String, throwable: Throwable? = null) { + if (BuildConfig.DEBUG) { + if (throwable == null) { + Log.e(tag, msg) + } else { + Log.e(tag, msg, throwable) + } + } + } + + fun d(tag: String, msg: String, throwable: Throwable? = null) { + if (BuildConfig.DEBUG) { + if (throwable == null) { + Log.d(tag, msg) + } else { + Log.d(tag, msg, throwable) + } + } + } + + fun i(tag: String, msg: String, throwable: Throwable? = null) { + if (BuildConfig.DEBUG) { + if (throwable == null) { + Log.i(tag, msg) + } else { + Log.i(tag, msg, throwable) + } + } + } + + fun w(tag: String, msg: String, throwable: Throwable? = null) { + if (BuildConfig.DEBUG) { + if (throwable == null) { + Log.w(tag, msg) + } else { + Log.w(tag, msg, throwable) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/DialogExtensions.kt b/app/src/main/java/io/legado/app/utils/DialogExtensions.kt new file mode 100755 index 000000000..471feae80 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/DialogExtensions.kt @@ -0,0 +1,83 @@ +package io.legado.app.utils + +import android.app.Dialog +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.core.view.forEach +import androidx.fragment.app.DialogFragment +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.filletBackground +import splitties.systemservices.windowManager + +fun AlertDialog.applyTint(): AlertDialog { + window?.setBackgroundDrawable(context.filletBackground) + val colorStateList = Selector.colorBuild() + .setDefaultColor(ThemeStore.accentColor(context)) + .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) + .create() + if (getButton(AlertDialog.BUTTON_NEGATIVE) != null) { + getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList) + } + if (getButton(AlertDialog.BUTTON_POSITIVE) != null) { + getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList) + } + if (getButton(AlertDialog.BUTTON_NEUTRAL) != null) { + getButton(AlertDialog.BUTTON_NEUTRAL).setTextColor(colorStateList) + } + window?.decorView?.post { + listView?.forEach { + it.applyTint(context.accentColor) + } + } + return this +} + +fun AlertDialog.requestInputMethod() { + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) +} + +fun DialogFragment.setLayout(widthMix: Float, heightMix: Float) { + dialog?.setLayout(widthMix, heightMix) +} + +fun Dialog.setLayout(widthMix: Float, heightMix: Float) { + val dm = context.windowManager.windowSize + window?.setLayout( + (dm.widthPixels * widthMix).toInt(), + (dm.heightPixels * heightMix).toInt() + ) +} + +fun DialogFragment.setLayout(width: Int, heightMix: Float) { + dialog?.setLayout(width, heightMix) +} + +fun Dialog.setLayout(width: Int, heightMix: Float) { + val dm = context.windowManager.windowSize + window?.setLayout( + width, + (dm.heightPixels * heightMix).toInt() + ) +} + +fun DialogFragment.setLayout(widthMix: Float, height: Int) { + dialog?.setLayout(widthMix, height) +} + +fun Dialog.setLayout(widthMix: Float, height: Int) { + val dm = context.windowManager.windowSize + window?.setLayout( + (dm.widthPixels * widthMix).toInt(), + height + ) +} + +fun DialogFragment.setLayout(width: Int, height: Int) { + dialog?.setLayout(width, height) +} + +fun Dialog.setLayout(width: Int, height: Int) { + window?.setLayout(width, height) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt new file mode 100755 index 000000000..827e6c5c2 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt @@ -0,0 +1,46 @@ +package io.legado.app.utils + +import androidx.documentfile.provider.DocumentFile + +@Suppress("MemberVisibilityCanBePrivate") +object DocumentUtils { + + fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean { + val parent = getDirDocument(root, *subDirs) ?: return false + return parent.findFile(fileName)?.exists() ?: false + } + + fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) { + val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs) + parent?.findFile(fileName)?.delete() + } + + fun createFileIfNotExist( + root: DocumentFile, + fileName: String, + vararg subDirs: String + ): DocumentFile? { + val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs) + return parent?.findFile(fileName) ?: parent?.createFile("", fileName) + } + + fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? { + var parent: DocumentFile? = root + for (subDirName in subDirs) { + val subDir = parent?.findFile(subDirName) + ?: parent?.createDirectory(subDirName) + parent = subDir + } + return parent + } + + fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? { + var parent = root + for (subDirName in subDirs) { + val subDir = parent.findFile(subDirName) + parent = subDir ?: return null + } + return parent + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/DrawableUtils.kt b/app/src/main/java/io/legado/app/utils/DrawableUtils.kt new file mode 100755 index 000000000..7f986f632 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/DrawableUtils.kt @@ -0,0 +1,55 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.TransitionDrawable +import androidx.annotation.ColorInt +import androidx.core.graphics.drawable.DrawableCompat + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused") +object DrawableUtils { + + fun createTransitionDrawable( + @ColorInt startColor: Int, + @ColorInt endColor: Int + ): TransitionDrawable { + return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor)) + } + + fun createTransitionDrawable(start: Drawable, end: Drawable): TransitionDrawable { + val drawables = arrayOfNulls(2) + + drawables[0] = start + drawables[1] = end + + return TransitionDrawable(drawables) + } + +} + +fun Drawable.setTintListMutate( + tint: ColorStateList, + tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP +) { + val wrappedDrawable = DrawableCompat.wrap(this) + wrappedDrawable.mutate() + DrawableCompat.setTintMode(wrappedDrawable, tintMode) + DrawableCompat.setTintList(wrappedDrawable, tint) +} + +fun Drawable.setTintMutate( + @ColorInt tint: Int, + tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP +) { + val wrappedDrawable = DrawableCompat.wrap(this) + wrappedDrawable.mutate() + DrawableCompat.setTintMode(wrappedDrawable, tintMode) + DrawableCompat.setTint(wrappedDrawable, tint) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt new file mode 100755 index 000000000..97f4d1dca --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt @@ -0,0 +1,51 @@ +package io.legado.app.utils + +import android.util.Base64 + +/** + * 编码工具 escape base64 + */ +@Suppress("unused") +object EncoderUtils { + + fun escape(src: String): String { + val tmp = StringBuilder() + for (char in src) { + val charCode = char.code + if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) { + tmp.append(char) + continue + } + + val prefix = when { + charCode < 16 -> "%0" + charCode < 256 -> "%" + else -> "%u" + } + tmp.append(prefix).append(charCode.toString(16)) + } + return tmp.toString() + } + + @JvmOverloads + fun base64Decode(str: String, flags: Int = Base64.DEFAULT): String { + val bytes = Base64.decode(str, flags) + return String(bytes) + } + + @JvmOverloads + fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { + return Base64.encodeToString(str.toByteArray(), flags) + } + + @JvmOverloads + fun base64Encode(bytes: ByteArray, flags: Int = Base64.NO_WRAP): String { + return Base64.encodeToString(bytes, flags) + } + + @JvmOverloads + fun base64DecodeToByteArray(str: String, flags: Int = Base64.DEFAULT): ByteArray { + return Base64.decode(str, flags) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/EncodingDetect.kt b/app/src/main/java/io/legado/app/utils/EncodingDetect.kt new file mode 100755 index 000000000..23eceb85a --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/EncodingDetect.kt @@ -0,0 +1,98 @@ +package io.legado.app.utils + +import android.text.TextUtils +import io.legado.app.lib.icu4j.CharsetDetector +import org.jsoup.Jsoup +import java.io.File + +/** + * 自动获取文件的编码 + * */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +object EncodingDetect { + + private val headTagRegex = "(?i)[\\s\\S]*?".toRegex() + private val headOpenBytes = "".toByteArray() + private val headCloseBytes = "".toByteArray() + + fun getHtmlEncode(bytes: ByteArray): String { + try { + var head: String? = null + val startIndex = bytes.indexOf(headOpenBytes) + if (startIndex > -1) { + val endIndex = bytes.indexOf(headCloseBytes, startIndex) + if (endIndex > -1) { + head = String(bytes.copyOfRange(startIndex, endIndex + headCloseBytes.size)) + } + } + val doc = Jsoup.parseBodyFragment(head ?: headTagRegex.find(String(bytes))!!.value) + val metaTags = doc.getElementsByTag("meta") + var charsetStr: String + for (metaTag in metaTags) { + charsetStr = metaTag.attr("charset") + if (!TextUtils.isEmpty(charsetStr)) { + return charsetStr + } + val httpEquiv = metaTag.attr("http-equiv") + if (httpEquiv.equals("content-type", true)) { + val content = metaTag.attr("content") + val idx = content.indexOf("charset=", ignoreCase = true) + charsetStr = if (idx > -1) { + content.substring(idx + "charset=".length) + } else { + content.substringAfter(";") + } + if (!TextUtils.isEmpty(charsetStr)) { + return charsetStr + } + } + } + } catch (ignored: Exception) { + } + return getEncode(bytes) + } + + fun getEncode(bytes: ByteArray): String { + val match = CharsetDetector().setText(bytes).detect() + return match?.name ?: "UTF-8" + } + + /** + * 得到文件的编码 + */ + fun getEncode(filePath: String): String { + return getEncode(File(filePath)) + } + + /** + * 得到文件的编码 + */ + fun getEncode(file: File): String { + val tempByte = getFileBytes(file) + if (tempByte.isEmpty()) { + return "UTF-8" + } + return getEncode(tempByte) + } + + private fun getFileBytes(file: File): ByteArray { + val byteArray = ByteArray(8000) + var pos = 0 + try { + file.inputStream().buffered().use { + while (pos < byteArray.size) { + val n = it.read(byteArray, pos, 1) + if (n == -1) { + break + } + if (byteArray[pos] < 0) { + pos++ + } + } + } + } catch (e: Exception) { + System.err.println("Error: $e") + } + return byteArray.copyOf(pos) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt b/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt new file mode 100755 index 000000000..13947c893 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/EventBusExtensions.kt @@ -0,0 +1,98 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.Observer +import com.jeremyliao.liveeventbus.LiveEventBus +import com.jeremyliao.liveeventbus.core.Observable + +inline fun eventObservable(tag: String): Observable { + return LiveEventBus.get(tag, EVENT::class.java) +} + +inline fun postEvent(tag: String, event: EVENT) { + LiveEventBus.get(tag).post(event) +} + +inline fun postEventDelay(tag: String, event: EVENT, delay: Long) { + LiveEventBus.get(tag).postDelay(event, delay) +} + +inline fun postEventOrderly(tag: String, event: EVENT) { + LiveEventBus.get(tag).postOrderly(event) +} + +inline fun AppCompatActivity.observeEvent( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observe(this, o) + } +} + +inline fun AppCompatActivity.observeEventSticky( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observeSticky(this, o) + } +} + +inline fun Fragment.observeEvent( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observe(this, o) + } +} + +inline fun Fragment.observeEventSticky( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observeSticky(this, o) + } +} + +inline fun LifecycleService.observeEvent( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observe(this, o) + } +} + +inline fun LifecycleService.observeEventSticky( + vararg tags: String, + noinline observer: (EVENT) -> Unit +) { + val o = Observer { + observer(it) + } + tags.forEach { + eventObservable(it).observeSticky(this, o) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt new file mode 100755 index 000000000..cf209fda8 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt @@ -0,0 +1,426 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.app.DownloadManager +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import io.legado.app.exception.NoStackTraceException +import splitties.init.appCtx +import splitties.systemservices.downloadManager +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.nio.charset.Charset +import java.util.concurrent.atomic.AtomicInteger + + +data class FileDoc( + val name: String, + val isDir: Boolean, + val size: Long, + val lastModified: Long, + val uri: Uri +) { + + override fun toString(): String { + return if (uri.isContentScheme()) uri.toString() else uri.path!! + } + + val isContentScheme get() = uri.isContentScheme() + + fun readBytes(): ByteArray { + return uri.readBytes(appCtx) + } + + fun readText(): String { + return uri.readText(appCtx) + } + + fun asDocumentFile(): DocumentFile? { + if (isContentScheme) { + return if (isDir) { + treeDocumentFileConstructor.newInstance(null, appCtx, uri) as DocumentFile + } else { + DocumentFile.fromSingleUri(appCtx, uri) + } + } + return null + } + + fun asFile(): File? { + if (isContentScheme) { + return null + } + return File(uri.path!!) + } + + companion object { + + private val treeDocumentFileConstructor by lazy { + Class.forName("androidx.documentfile.provider.TreeDocumentFile") + .getDeclaredConstructor( + DocumentFile::class.java, + Context::class.java, + Uri::class.java + ).apply { + isAccessible = true + } + } + + fun fromDir(path: String): FileDoc { + return fromUri(path.toUri(), true) + } + + fun fromFile(path: String): FileDoc { + return fromUri(path.toUri(), false) + } + + fun fromDir(uri: Uri): FileDoc { + return fromUri(uri, true) + } + + fun fromUri(uri: Uri, isDir: Boolean): FileDoc { + if (uri.isContentScheme()) { + val doc = if (isDir) { + DocumentFile.fromTreeUri(appCtx, uri)!! + } else if (uri.host == "downloads") { + val query = DownloadManager.Query() + query.setFilterById(uri.lastPathSegment!!.toLong()) + downloadManager.query(query).use { + if (it.moveToFirst()) { + val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) + val lUri = it.getString(lUriColum) + DocumentFile.fromSingleUri(appCtx, Uri.parse(lUri))!! + } else { + DocumentFile.fromSingleUri(appCtx, uri)!! + } + } + } else { + DocumentFile.fromSingleUri(appCtx, uri)!! + } + return FileDoc(doc.name ?: "", isDir, doc.length(), doc.lastModified(), doc.uri) + } + val file = File(uri.path!!) + return FileDoc(file.name, isDir, file.length(), file.lastModified(), uri) + } + + fun fromDocumentFile(doc: DocumentFile): FileDoc { + return FileDoc( + name = doc.name ?: "", + isDir = doc.isDirectory, + size = doc.length(), + lastModified = doc.lastModified(), + uri = doc.uri + ) + } + + fun fromFile(file: File): FileDoc { + return FileDoc( + name = file.name, + isDir = file.isDirectory, + size = file.length(), + lastModified = file.lastModified(), + uri = Uri.fromFile(file) + ) + } + + } +} + +/** + * 过滤器 + */ +typealias FileDocFilter = (file: FileDoc) -> Boolean + +private val projection by lazy { + arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_MIME_TYPE + ) +} + +/** + * 返回子文件列表,如果不是文件夹则返回null + */ +fun FileDoc.list(filter: FileDocFilter? = null): ArrayList? { + if (isDir) { + if (uri.isContentScheme()) { + /** + * DocumentFile 的 listFiles() 非常的慢,所以这里直接从数据库查询 + */ + val childrenUri = DocumentsContract + .buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri)) + val docList = arrayListOf() + var cursor: Cursor? = null + try { + cursor = appCtx.contentResolver.query( + childrenUri, + projection, + null, + null, + DocumentsContract.Document.COLUMN_DISPLAY_NAME + ) + cursor?.let { + val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE) + val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE) + val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + if (cursor.moveToFirst()) { + do { + val item = FileDoc( + name = cursor.getString(nci), + isDir = cursor.getString(mci) == + DocumentsContract.Document.MIME_TYPE_DIR, + size = cursor.getLong(sci), + lastModified = cursor.getLong(dci), + uri = DocumentsContract.buildDocumentUriUsingTree( + uri, + cursor.getString(ici) + ) + ) + if (filter == null || filter.invoke(item)) { + docList.add(item) + } + } while (cursor.moveToNext()) + } + } + } finally { + cursor?.close() + } + return docList + } else { + return File(uri.path!!).listFileDocs(filter) + } + } + return null +} + +/** + * 查找文档, 如果存在则返回文档,如果不存在返回空 + * @param name 文件名 + * @param depth 查找文件夹深度 + */ +fun FileDoc.find(name: String, depth: Int = 0): FileDoc? { + val list = list() + list?.forEach { + if (it.name == name) { + return it + } + } + if (depth > 0) { + list?.forEach { + if (it.isDir) { + val fileDoc = it.find(name, depth - 1) + if (fileDoc != null) { + return fileDoc + } + } + } + } + return null +} + +/** + * 查找文档, 如果存在则返回文档,如果不存在返回空 + * @param name 文件名 + * @param depth 查找文件夹深度 + * @param maxFinds 最大查找文件夹数量 + */ +fun FileDoc.find(name: String, depth: Int = 0, maxFinds: Int = Int.MAX_VALUE): FileDoc? { + return find(name, depth, AtomicInteger(maxFinds)) +} + +private fun FileDoc.find(name: String, depth: Int, maxFinds: AtomicInteger): FileDoc? { + if (maxFinds.getAndDecrement() <= 0) { + return null + } + val list = list() + list?.forEach { + if (it.name == name) { + return it + } + } + if (depth > 0) { + list?.forEach { + if (it.isDir) { + val fileDoc = it.find(name, depth - 1, maxFinds) + if (fileDoc != null) { + return fileDoc + } + } + } + } + return null +} + +fun FileDoc.createFileIfNotExist( + fileName: String, + vararg subDirs: String +): FileDoc { + return if (uri.isContentScheme()) { + val documentFile = asDocumentFile()!! + val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, *subDirs)!! + FileDoc.fromDocumentFile(tmp) + } else { + val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName + val tmp = FileUtils.createFileIfNotExist(path) + FileDoc.fromFile(tmp) + } +} + +fun FileDoc.createFolderIfNotExist( + vararg subDirs: String +): FileDoc { + return if (uri.isContentScheme()) { + val documentFile = asDocumentFile()!! + val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!! + FileDoc.fromDocumentFile(tmp) + } else { + val path = FileUtils.getPath(uri.path!!, *subDirs) + val tmp = FileUtils.createFolderIfNotExist(path) + FileDoc.fromFile(tmp) + } +} + +fun FileDoc.openInputStream(): Result { + return uri.inputStream(appCtx) +} + +fun FileDoc.openOutputStream(): Result { + return uri.outputStream(appCtx) +} + +fun FileDoc.openReadPfd(): Result { + return uri.toReadPfd(appCtx) +} + +fun FileDoc.openWritePfd(): Result { + return uri.toWritePfd(appCtx) +} + +fun FileDoc.exists( + fileName: String, + vararg subDirs: String +): Boolean { + return if (uri.isContentScheme()) { + DocumentUtils.exists(asDocumentFile()!!, fileName, *subDirs) + } else { + val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName + FileUtils.exist(path) + } +} + +fun FileDoc.exists(): Boolean { + return if (uri.isContentScheme()) { + asDocumentFile()!!.exists() + } else { + FileUtils.exist(uri.path!!) + } +} + +fun FileDoc.writeText(text: String) { + if (uri.isContentScheme()) { + uri.writeText(appCtx, text) + } else { + File(uri.path!!).writeText(text) + } +} + +fun FileDoc.writeFile(file: File) { + openOutputStream().getOrThrow().use { out -> + file.inputStream().use { + it.copyTo(out) + } + } +} + +fun FileDoc.delete() { + asFile()?.let { + FileUtils.delete(it, true) + } + asDocumentFile()?.delete() +} + +fun FileDoc.checkWrite(): Boolean { + if (!isDir) { + throw NoStackTraceException("只能检查目录") + } + asFile()?.let { + return it.checkWrite() + } + return asDocumentFile()!!.checkWrite() +} + +/** + * DocumentFile 的 listFiles() 非常的慢,尽量不要使用 + */ +fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList? { + return FileDoc.fromDocumentFile(this).list(filter) +} + +@Throws(Exception::class) +fun DocumentFile.openInputStream(): InputStream? { + return appCtx.contentResolver.openInputStream(uri) +} + +@Throws(Exception::class) +fun DocumentFile.openOutputStream(): OutputStream? { + return appCtx.contentResolver.openOutputStream(uri) +} + +@Throws(Exception::class) +fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) { + uri.writeText(context, data, charset) +} + +@Throws(Exception::class) +fun DocumentFile.writeBytes(context: Context, data: ByteArray) { + uri.writeBytes(context, data) +} + +@Throws(Exception::class) +fun DocumentFile.readText(context: Context): String { + return String(readBytes(context)) +} + +@Throws(Exception::class) +fun DocumentFile.readBytes(context: Context): ByteArray { + return context.contentResolver.openInputStream(uri)?.let { + val len: Int = it.available() + val buffer = ByteArray(len) + it.read(buffer) + it.close() + return buffer + } ?: throw NoStackTraceException("打开文件失败\n${uri}") +} + +fun DocumentFile.checkWrite(): Boolean { + var file: DocumentFile? = null + return try { + val filename = System.currentTimeMillis().toString() + file = createFile(FileUtils.getMimeType(filename), filename) + file?.openOutputStream()?.let { out -> + out.bufferedWriter().use { it.write(filename) } + file.openInputStream()?.let { input -> + input.bufferedReader().use { + return it.readText() == filename + } + } + } + false + } catch (e: Exception) { + false + } finally { + file?.delete() + } +} diff --git a/app/src/main/java/io/legado/app/utils/FileExtensions.kt b/app/src/main/java/io/legado/app/utils/FileExtensions.kt new file mode 100755 index 000000000..1bdf2d7d7 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FileExtensions.kt @@ -0,0 +1,88 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.net.Uri +import java.io.File +import java.io.FileOutputStream + +fun File.getFile(vararg subDirFiles: String): File { + val path = FileUtils.getPath(this, *subDirFiles) + return File(path) +} + +fun File.exists(vararg subDirFiles: String): Boolean { + return getFile(*subDirFiles).exists() +} + +@Throws(Exception::class) +fun File.listFileDocs(filter: FileDocFilter? = null): ArrayList { + val docList = arrayListOf() + listFiles()?.forEach { + val item = FileDoc( + it.name, + it.isDirectory, + it.length(), + it.lastModified(), + Uri.fromFile(it) + ) + if (filter == null || filter.invoke(item)) { + docList.add(item) + } + } + return docList +} + +fun File.createFileIfNotExist(): File { + if (!exists()) { + parentFile?.createFolderIfNotExist() + createNewFile() + } + return this +} + +fun File.createFileReplace(): File { + if (!exists()) { + parent?.let { + File(it).mkdirs() + } + createNewFile() + } else { + delete() + createNewFile() + } + return this +} + +fun File.createFolderIfNotExist(): File { + if (!exists()) { + mkdirs() + } + return this +} + +fun File.createFolderReplace(): File { + if (exists()) { + FileUtils.delete(this, true) + } + mkdirs() + return this +} + +fun File.checkWrite(): Boolean { + var file: File? = null + return try { + val filename = System.currentTimeMillis().toString() + file = FileUtils.createFileIfNotExist(this, filename) + file.outputStream().bufferedWriter().use { it.write(filename) } + file.inputStream().bufferedReader().use { it.readText() == filename } + } catch (e: Exception) { + false + } finally { + file?.delete() + } +} + +fun File.outputStream(append: Boolean = false): FileOutputStream { + return FileOutputStream(this, append) +} diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt new file mode 100755 index 000000000..6cf3255d2 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt @@ -0,0 +1,763 @@ +package io.legado.app.utils + +import android.os.Environment +import android.webkit.MimeTypeMap +import androidx.annotation.IntDef +import splitties.init.appCtx +import java.io.* +import java.nio.charset.Charset +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Pattern + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object FileUtils { + + fun createFileIfNotExist(root: File, vararg subDirFiles: String): File { + val filePath = getPath(root, *subDirFiles) + return createFileIfNotExist(filePath) + } + + fun createFolderIfNotExist(root: File, vararg subDirs: String): File { + val filePath = getPath(root, *subDirs) + return createFolderIfNotExist(filePath) + } + + fun createFolderIfNotExist(filePath: String): File { + val file = File(filePath) + //如果文件夹不存在,就创建它 + if (!file.exists()) { + file.mkdirs() + } + return file + } + + @Synchronized + fun createFileIfNotExist(filePath: String): File { + val file = File(filePath) + try { + if (!file.exists()) { + //创建父类文件夹 + file.parent?.let { + createFolderIfNotExist(it) + } + //创建文件 + file.createNewFile() + } + } catch (e: IOException) { + e.printOnDebug() + } + return file + } + + fun createFileWithReplace(filePath: String): File { + val file = File(filePath) + if (!file.exists()) { + //创建父类文件夹 + file.parent?.let { + createFolderIfNotExist(it) + } + //创建文件 + file.createNewFile() + } else { + file.delete() + file.createNewFile() + } + return file + } + + fun getPath(rootPath: String, vararg subDirFiles: String): String { + val path = StringBuilder(rootPath) + subDirFiles.forEach { + if (it.isNotEmpty()) { + if (!path.endsWith(File.separator)) { + path.append(File.separator) + } + path.append(it) + } + } + return path.toString() + } + + fun getPath(root: File, vararg subDirFiles: String): String { + val path = StringBuilder(root.absolutePath) + subDirFiles.forEach { + if (it.isNotEmpty()) { + path.append(File.separator).append(it) + } + } + return path.toString() + } + + fun getCachePath(): String { + return appCtx.externalCache.absolutePath + } + + fun getSdCardPath(): String { + var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath + try { + sdCardDirectory = File(sdCardDirectory).canonicalPath + } catch (e: IOException) { + e.printOnDebug() + } + return sdCardDirectory + } + + const val BY_NAME_ASC = 0 + const val BY_NAME_DESC = 1 + const val BY_TIME_ASC = 2 + const val BY_TIME_DESC = 3 + const val BY_SIZE_ASC = 4 + const val BY_SIZE_DESC = 5 + const val BY_EXTENSION_ASC = 6 + const val BY_EXTENSION_DESC = 7 + + @IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC]) + @Retention(AnnotationRetention.SOURCE) + annotation class SortType + + /** + * 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符 + */ + fun separator(path: String): String { + var path1 = path + val separator = File.separator + path1 = path1.replace("\\", separator) + if (!path1.endsWith(separator)) { + path1 += separator + } + return path1 + } + + fun closeSilently(c: Closeable?) { + if (c == null) { + return + } + try { + c.close() + } catch (ignored: IOException) { + } + + } + + /** + * 列出指定目录下的所有子目录 + */ + @JvmOverloads + fun listDirs( + startDirPath: String, + excludeDirs: Array? = null, @SortType sortType: Int = BY_NAME_ASC + ): Array { + var excludeDirs1 = excludeDirs + val dirList = ArrayList() + val startDir = File(startDirPath) + if (!startDir.isDirectory) { + return arrayOf() + } + val dirs = startDir.listFiles(FileFilter { f -> + if (f == null) { + return@FileFilter false + } + f.isDirectory + }) ?: return arrayOf() + if (excludeDirs1 == null) { + excludeDirs1 = arrayOf() + } + for (dir in dirs) { + val file = dir.absoluteFile + if (!excludeDirs1.contentDeepToString().contains(file.name)) { + dirList.add(file) + } + } + when (sortType) { + BY_NAME_ASC -> Collections.sort(dirList, SortByName()) + BY_NAME_DESC -> { + Collections.sort(dirList, SortByName()) + dirList.reverse() + } + BY_TIME_ASC -> Collections.sort(dirList, SortByTime()) + BY_TIME_DESC -> { + Collections.sort(dirList, SortByTime()) + dirList.reverse() + } + BY_SIZE_ASC -> Collections.sort(dirList, SortBySize()) + BY_SIZE_DESC -> { + Collections.sort(dirList, SortBySize()) + dirList.reverse() + } + BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension()) + BY_EXTENSION_DESC -> { + Collections.sort(dirList, SortByExtension()) + dirList.reverse() + } + } + return dirList.toTypedArray() + } + + /** + * 列出指定目录下的所有子目录及所有文件 + */ + @JvmOverloads + fun listDirsAndFiles( + startDirPath: String, + allowExtensions: Array? = null + ): Array? { + val dirs: Array? + val files: Array? = if (allowExtensions == null) { + listFiles(startDirPath) + } else { + listFiles(startDirPath, allowExtensions) + } + dirs = listDirs(startDirPath) + if (files == null) { + return null + } + return dirs + files + } + + /** + * 列出指定目录下的所有文件 + */ + @JvmOverloads + fun listFiles( + startDirPath: String, + filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC + ): Array { + val fileList = ArrayList() + val f = File(startDirPath) + if (!f.isDirectory) { + return arrayOf() + } + val files = f.listFiles(FileFilter { file -> + if (file == null) { + return@FileFilter false + } + if (file.isDirectory) { + return@FileFilter false + } + + filterPattern?.matcher(file.name)?.find() ?: true + }) + ?: return arrayOf() + for (file in files) { + fileList.add(file.absoluteFile) + } + when (sortType) { + BY_NAME_ASC -> Collections.sort(fileList, SortByName()) + BY_NAME_DESC -> { + Collections.sort(fileList, SortByName()) + fileList.reverse() + } + BY_TIME_ASC -> Collections.sort(fileList, SortByTime()) + BY_TIME_DESC -> { + Collections.sort(fileList, SortByTime()) + fileList.reverse() + } + BY_SIZE_ASC -> Collections.sort(fileList, SortBySize()) + BY_SIZE_DESC -> { + Collections.sort(fileList, SortBySize()) + fileList.reverse() + } + BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension()) + BY_EXTENSION_DESC -> { + Collections.sort(fileList, SortByExtension()) + fileList.reverse() + } + } + return fileList.toTypedArray() + } + + /** + * 列出指定目录下的所有文件 + */ + fun listFiles(startDirPath: String, allowExtensions: Array?): Array? { + val file = File(startDirPath) + return file.listFiles { _, name -> + //返回当前目录所有以某些扩展名结尾的文件 + val extension = getExtension(name) + allowExtensions?.contentDeepToString()?.contains(extension) == true + || allowExtensions == null + } + } + + /** + * 列出指定目录下的所有文件 + */ + fun listFiles(startDirPath: String, allowExtension: String?): Array? { + return if (allowExtension == null) + listFiles(startDirPath, allowExtension = null) + else + listFiles(startDirPath, arrayOf(allowExtension)) + } + + /** + * 判断文件或目录是否存在 + */ + fun exist(path: String): Boolean { + val file = File(path) + return file.exists() + } + + /** + * 删除文件或目录 + */ + @JvmOverloads + fun delete(file: File, deleteRootDir: Boolean = false): Boolean { + var result = false + if (file.isFile) { + //是文件 + result = deleteResolveEBUSY(file) + } else { + //是目录 + val files = file.listFiles() ?: return false + if (files.isEmpty()) { + result = deleteRootDir && deleteResolveEBUSY(file) + } else { + for (f in files) { + delete(f, deleteRootDir) + result = deleteResolveEBUSY(f) + } + } + if (deleteRootDir) { + result = deleteResolveEBUSY(file) + } + } + return result + } + + /** + * bug: open failed: EBUSY (Device or resource busy) + * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy + */ + private fun deleteResolveEBUSY(file: File): Boolean { + // Before you delete a Directory or File: rename it! + val to = File(file.absolutePath + System.currentTimeMillis()) + + file.renameTo(to) + return to.delete() + } + + /** + * 删除文件或目录 + */ + @JvmOverloads + fun delete(path: String, deleteRootDir: Boolean = true): Boolean { + val file = File(path) + + return if (file.exists()) { + delete(file, deleteRootDir) + } else false + } + + /** + * 复制文件为另一个文件,或复制某目录下的所有文件及目录到另一个目录下 + */ + fun copy(src: String, tar: String): Boolean { + val srcFile = File(src) + return srcFile.exists() && copy(srcFile, File(tar)) + } + + /** + * 复制文件或目录 + */ + fun copy(src: File, tar: File): Boolean { + try { + if (src.isFile) { + val inputStream = FileInputStream(src) + val outputStream = FileOutputStream(tar) + inputStream.use { + outputStream.use { + inputStream.copyTo(outputStream) + outputStream.flush() + } + } + } else if (src.isDirectory) { + tar.mkdirs() + src.listFiles()?.forEach { file -> + copy(file.absoluteFile, File(tar.absoluteFile, file.name)) + } + } + return true + } catch (e: Exception) { + return false + } + + } + + /** + * 移动文件或目录 + */ + fun move(src: String, tar: String): Boolean { + return move(File(src), File(tar)) + } + + /** + * 移动文件或目录 + */ + fun move(src: File, tar: File): Boolean { + return rename(src, tar) + } + + /** + * 文件重命名 + */ + fun rename(oldPath: String, newPath: String): Boolean { + return rename(File(oldPath), File(newPath)) + } + + /** + * 文件重命名 + */ + fun rename(src: File, tar: File): Boolean { + return src.renameTo(tar) + } + + /** + * 读取文本文件, 失败将返回空串 + */ + @JvmOverloads + fun readText(filepath: String, charset: String = "utf-8"): String { + try { + val data = readBytes(filepath) + if (data != null) { + return String(data, Charset.forName(charset)).trim { it <= ' ' } + } + } catch (ignored: UnsupportedEncodingException) { + } + + return "" + } + + /** + * 读取文件内容, 失败将返回空串 + */ + fun readBytes(filepath: String): ByteArray? { + var fis: FileInputStream? = null + try { + fis = FileInputStream(filepath) + val outputStream = ByteArrayOutputStream() + val buffer = ByteArray(1024) + while (true) { + val len = fis.read(buffer, 0, buffer.size) + if (len == -1) { + break + } else { + outputStream.write(buffer, 0, len) + } + } + val data = outputStream.toByteArray() + outputStream.close() + return data + } catch (e: IOException) { + return null + } finally { + closeSilently(fis) + } + } + + /** + * 保存文本内容 + */ + @JvmOverloads + fun writeText(filepath: String, content: String, charset: String = "utf-8"): Boolean { + return try { + writeBytes(filepath, content.toByteArray(charset(charset))) + } catch (e: UnsupportedEncodingException) { + false + } + + } + + /** + * 保存文件内容 + */ + fun writeBytes(filepath: String, data: ByteArray): Boolean { + val file = File(filepath) + var fos: FileOutputStream? = null + return try { + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } + fos = FileOutputStream(filepath) + fos.write(data) + true + } catch (e: IOException) { + false + } finally { + closeSilently(fos) + } + } + + /** + * 保存文件内容 + */ + fun writeInputStream(filepath: String, data: InputStream): Boolean { + val file = File(filepath) + return writeInputStream(file, data) + } + + /** + * 保存文件内容 + */ + fun writeInputStream(file: File, data: InputStream): Boolean { + return try { + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } + data.use { + FileOutputStream(file).use { fos -> + data.copyTo(fos) + fos.flush() + } + } + true + } catch (e: IOException) { + false + } + } + + /** + * 追加文本内容 + */ + fun appendText(path: String, content: String): Boolean { + val file = File(path) + var writer: FileWriter? = null + return try { + if (!file.exists()) { + file.createNewFile() + } + writer = FileWriter(file, true) + writer.write(content) + true + } catch (e: IOException) { + false + } finally { + closeSilently(writer) + } + } + + /** + * 获取文件大小 + */ + fun getLength(path: String): Long { + val file = File(path) + return if (!file.isFile || !file.exists()) { + 0 + } else file.length() + } + + /** + * 获取文件或网址的名称(包括后缀) + */ + fun getName(path: String?): String { + if (path == null) { + return "" + } + val pos = path.lastIndexOf(File.separator) + return if (0 <= pos) { + path.substring(pos + 1) + } else { + path + } + } + + /** + * 获取文件名(不包括扩展名) + */ + fun getNameExcludeExtension(path: String): String { + return try { + var fileName = File(path).name + val lastIndexOf = fileName.lastIndexOf(".") + if (lastIndexOf != -1) { + fileName = fileName.substring(0, lastIndexOf) + } + fileName + } catch (e: Exception) { + "" + } + + } + + /** + * 获取格式化后的文件大小 + */ + fun getSize(path: String): String { + val fileSize = getLength(path) + return ConvertUtils.formatFileSize(fileSize) + } + + /** + * 获取文件后缀,不包括“.” + */ + fun getExtension(pathOrUrl: String): String { + val dotPos = pathOrUrl.lastIndexOf('.') + return if (0 <= dotPos) { + pathOrUrl.substring(dotPos + 1) + } else { + "ext" + } + } + + /** + * 获取文件的MIME类型 + */ + fun getMimeType(pathOrUrl: String): String { + val ext = getExtension(pathOrUrl) + val map = MimeTypeMap.getSingleton() + return map.getMimeTypeFromExtension(ext) ?: "*/*" + } + + /** + * 获取格式化后的文件/目录创建或最后修改时间 + */ + @JvmOverloads + fun getDateTime(path: String, format: String = "yyyy年MM月dd日HH:mm"): String { + val file = File(path) + return getDateTime(file, format) + } + + /** + * 获取格式化后的文件/目录创建或最后修改时间 + */ + fun getDateTime(file: File, format: String): String { + val cal = Calendar.getInstance() + cal.timeInMillis = file.lastModified() + return SimpleDateFormat(format, Locale.PRC).format(cal.time) + } + + /** + * 比较两个文件的最后修改时间 + */ + fun compareLastModified(path1: String, path2: String): Int { + val stamp1 = File(path1).lastModified() + val stamp2 = File(path2).lastModified() + return when { + stamp1 > stamp2 -> 1 + stamp1 < stamp2 -> -1 + else -> 0 + } + } + + /** + * 创建多级别的目录 + */ + fun makeDirs(path: String): Boolean { + return makeDirs(File(path)) + } + + /** + * 创建多级别的目录 + */ + fun makeDirs(file: File): Boolean { + return file.mkdirs() + } + + class SortByExtension : Comparator { + + override fun compare(f1: File?, f2: File?): Int { + return if (f1 == null || f2 == null) { + if (f1 == null) -1 else 1 + } else { + if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + f1.name.compareTo(f2.name, ignoreCase = true) + } + } + } + + } + + class SortByName : Comparator { + private var caseSensitive: Boolean = false + + constructor(caseSensitive: Boolean) { + this.caseSensitive = caseSensitive + } + + constructor() { + this.caseSensitive = false + } + + override fun compare(f1: File?, f2: File?): Int { + if (f1 == null || f2 == null) { + return if (f1 == null) { + -1 + } else { + 1 + } + } else { + return if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + val s1 = f1.name + val s2 = f2.name + if (caseSensitive) { + s1.cnCompare(s2) + } else { + s1.compareTo(s2, ignoreCase = true) + } + } + } + } + + } + + class SortBySize : Comparator { + + override fun compare(f1: File?, f2: File?): Int { + return if (f1 == null || f2 == null) { + if (f1 == null) { + -1 + } else { + 1 + } + } else { + if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + if (f1.length() < f2.length()) { + -1 + } else { + 1 + } + } + } + } + + } + + class SortByTime : Comparator { + + override fun compare(f1: File?, f2: File?): Int { + return if (f1 == null || f2 == null) { + if (f1 == null) { + -1 + } else { + 1 + } + } else { + if (f1.isDirectory && f2.isFile) { + -1 + } else if (f1.isFile && f2.isDirectory) { + 1 + } else { + if (f1.lastModified() > f2.lastModified()) { + -1 + } else { + 1 + } + } + } + } + + } +} diff --git a/app/src/main/java/io/legado/app/utils/FlowExtensions.kt b/app/src/main/java/io/legado/app/utils/FlowExtensions.kt new file mode 100755 index 000000000..40a7d71db --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FlowExtensions.kt @@ -0,0 +1,263 @@ +package io.legado.app.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import io.legado.app.data.appDb +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.produceIn +import kotlinx.coroutines.sync.Semaphore + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow.onEachParallel( + concurrency: Int, + crossinline action: suspend (T) -> Unit +): Flow = flatMapMerge(concurrency) { value -> + flow { + action(value) + emit(value) + } +}.buffer(0) + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow.onEachParallelSafe( + concurrency: Int, + crossinline action: suspend (T) -> Unit +): Flow = flatMapMerge(concurrency) { value -> + flow { + try { + action(value) + } catch (e: Throwable) { + currentCoroutineContext().ensureActive() + } + emit(value) + } +}.buffer(0) + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow.mapParallel( + concurrency: Int, + crossinline transform: suspend (T) -> R, +): Flow = flatMapMerge(concurrency) { value -> flow { emit(transform(value)) } }.buffer(0) + + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow.mapParallelSafe( + concurrency: Int, + crossinline transform: suspend (T) -> R, +): Flow = flatMapMerge(concurrency) { value -> + flow { + try { + emit(transform(value)) + } catch (_: Throwable) { + currentCoroutineContext().ensureActive() + } + } +}.buffer(0) + +@OptIn(ExperimentalCoroutinesApi::class) +inline fun Flow.transformParallelSafe( + concurrency: Int, + crossinline transform: suspend FlowCollector.(T) -> R, +): Flow = flatMapMerge(concurrency) { value -> + flow { + try { + transform(value) + } catch (e: Throwable) { + currentCoroutineContext().ensureActive() + } + } +}.buffer(0) + +inline fun Flow.mapNotNullParallel( + concurrency: Int, + crossinline transform: suspend (T) -> R?, +): Flow = mapParallel(concurrency, transform).filterNotNull() + +inline fun Flow.onEachIndexed( + crossinline action: suspend (index: Int, T) -> Unit, +): Flow = flow { + var index = 0 + collect { value -> + action(index++, value) + emit(value) + } +} + +inline fun Flow.mapIndexed( + crossinline action: suspend (index: Int, T) -> R, +): Flow = flow { + var index = 0 + collect { value -> + emit(action(index++, value)) + } +} + +inline fun Flow.mapAsync( + concurrency: Int, + crossinline transform: suspend (T) -> R +): Flow = if (concurrency == 1) { + map { transform(it) } +} else { + Semaphore(concurrency).let { semaphore -> + channelFlow { + collect { + semaphore.acquire() + send(async { transform(it) }) + } + }.map { + it.await() + }.onEach { semaphore.release() } + }.buffer(0) +} + +inline fun Flow.mapAsyncIndexed( + concurrency: Int, + crossinline transform: suspend (index: Int, T) -> R +): Flow = if (concurrency == 1) { + mapIndexed { index, value -> + transform(index, value) + } +} else { + Semaphore(concurrency).let { semaphore -> + channelFlow { + var index = 0 + collect { + semaphore.acquire() + val i = index++ + send(async { transform(i, it) }) + } + }.map { + it.await() + }.onEach { semaphore.release() } + }.buffer(0) +} + +inline fun Flow.onEachAsync( + concurrency: Int, + crossinline action: suspend (T) -> Unit +): Flow = if (concurrency == 1) { + onEach { action(it) } +} else { + Semaphore(concurrency).let { semaphore -> + channelFlow { + collect { + semaphore.acquire() + send(async { + action(it) + it + }) + } + }.map { + it.await() + }.onEach { semaphore.release() } + }.buffer(0) +} + +inline fun Flow.onEachAsyncIndexed( + concurrency: Int, + crossinline action: suspend (index: Int, T) -> Unit +): Flow = if (concurrency == 1) { + onEachIndexed { index, value -> + action(index, value) + } +} else { + Semaphore(concurrency).let { semaphore -> + channelFlow { + var index = 0 + collect { + semaphore.acquire() + val i = index++ + send(async { + action(i, it) + it + }) + } + }.map { + it.await() + }.onEach { semaphore.release() } + }.buffer(0) +} + +fun Flow.flowWithLifecycleFirst( + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED +): Flow = callbackFlow { + if (!lifecycle.currentState.isAtLeast(minActiveState)) { + firstOrNull()?.let { + send(it) + } + } + lifecycle.repeatOnLifecycle(minActiveState) { + this@flowWithLifecycleFirst.collect { + send(it) + } + } + close() +} + +fun Flow.flowWithLifecycleAndDatabaseChange( + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + table: String +): Flow = callbackFlow { + var update = 0 + val channel = appDb.invalidationTracker + .createFlow(table) + .conflate() + .onEach { update++ } + .produceIn(this) + lifecycle.repeatOnLifecycle(minActiveState) { + if (update == 0) { + channel.receive() + } + this@flowWithLifecycleAndDatabaseChange.collect { + update = 0 + send(it) + } + } + close() +} + +fun Flow.flowWithLifecycleAndDatabaseChangeFirst( + lifecycle: Lifecycle, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + table: String +): Flow = callbackFlow { + var update = 0 + val isActive = lifecycle.currentState.isAtLeast(minActiveState) + val channel = appDb.invalidationTracker + .createFlow(table, emitInitialState = isActive) + .conflate() + .onEach { update++ } + .produceIn(this) + if (!isActive) { + firstOrNull()?.let { + send(it) + } + } + lifecycle.repeatOnLifecycle(minActiveState) { + if (update == 0) { + channel.receive() + } + this@flowWithLifecycleAndDatabaseChangeFirst.collect { + update = 0 + send(it) + } + } + close() +} diff --git a/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt b/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt new file mode 100755 index 000000000..62c03da5b --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt @@ -0,0 +1,114 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.app.Activity +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.os.Bundle +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.edit +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import io.legado.app.R +import io.legado.app.data.entities.Book +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isImage +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.ui.book.audio.AudioPlayActivity +import io.legado.app.ui.book.manga.ReadMangaActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.widget.dialog.TextDialog + +inline fun Fragment.showDialogFragment( + arguments: Bundle.() -> Unit = {} +) { + @Suppress("DEPRECATION") + val dialog = T::class.java.newInstance() + val bundle = Bundle() + bundle.apply(arguments) + dialog.arguments = bundle + dialog.show(childFragmentManager, T::class.simpleName) +} + +fun Fragment.showDialogFragment(dialogFragment: DialogFragment) { + dialogFragment.show(childFragmentManager, dialogFragment::class.simpleName) +} + +fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) = + requireContext().defaultSharedPreferences.getBoolean(key, defValue) + +fun Fragment.putPrefBoolean(key: String, value: Boolean = false) = + requireContext().defaultSharedPreferences.edit { putBoolean(key, value) } + +fun Fragment.getPrefInt(key: String, defValue: Int = 0) = + requireContext().defaultSharedPreferences.getInt(key, defValue) + +fun Fragment.putPrefInt(key: String, value: Int) = + requireContext().defaultSharedPreferences.edit { putInt(key, value) } + +fun Fragment.getPrefLong(key: String, defValue: Long = 0L) = + requireContext().defaultSharedPreferences.getLong(key, defValue) + +fun Fragment.putPrefLong(key: String, value: Long) = + requireContext().defaultSharedPreferences.edit { putLong(key, value) } + +fun Fragment.getPrefString(key: String, defValue: String? = null) = + requireContext().defaultSharedPreferences.getString(key, defValue) + +fun Fragment.putPrefString(key: String, value: String) = + requireContext().defaultSharedPreferences.edit { putString(key, value) } + +fun Fragment.getPrefStringSet( + key: String, + defValue: MutableSet? = null +): MutableSet? = + requireContext().defaultSharedPreferences.getStringSet(key, defValue) + +fun Fragment.putPrefStringSet(key: String, value: MutableSet) = + requireContext().defaultSharedPreferences.edit { putStringSet(key, value) } + +fun Fragment.removePref(key: String) = + requireContext().defaultSharedPreferences.edit { remove(key) } + +fun Fragment.getCompatColor(@ColorRes id: Int): Int = requireContext().getCompatColor(id) + +fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? = + requireContext().getCompatDrawable(id) + +fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = + requireContext().getCompatColorStateList(id) + +inline fun Fragment.startActivity( + configIntent: Intent.() -> Unit = {} +) { + startActivity(Intent(requireContext(), T::class.java).apply(configIntent)) +} + +fun Fragment.startActivityForBook( + book: Book, + configIntent: Intent.() -> Unit = {}, +) { + val cls = when { + book.isAudio -> AudioPlayActivity::class.java + !book.isLocal && book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java + else -> ReadBookActivity::class.java + } + val intent = Intent(requireActivity(), cls) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.putExtra("bookUrl", book.bookUrl) + intent.apply(configIntent) + startActivity(intent) +} + +fun Fragment.showHelp(fileName: String) { + val mdText = String(requireContext().assets.open("web/help/md/${fileName}.md").readBytes()) + showDialogFragment(TextDialog(getString(R.string.help), mdText, TextDialog.Mode.MD)) +} + +val Fragment.isCreated + get() = lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) diff --git a/app/src/main/java/io/legado/app/utils/GsonExtensions.kt b/app/src/main/java/io/legado/app/utils/GsonExtensions.kt new file mode 100755 index 000000000..1b8e3cf3c --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/GsonExtensions.kt @@ -0,0 +1,244 @@ +package io.legado.app.utils + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonSyntaxException +import com.google.gson.Strictness +import com.google.gson.ToNumberPolicy +import com.google.gson.internal.LinkedTreeMap +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonWriter +import io.legado.app.data.entities.rule.BookInfoRule +import io.legado.app.data.entities.rule.ContentRule +import io.legado.app.data.entities.rule.ExploreRule +import io.legado.app.data.entities.rule.ReviewRule +import io.legado.app.data.entities.rule.SearchRule +import io.legado.app.data.entities.rule.TocRule +import java.io.InputStream +import java.io.InputStreamReader +import java.io.OutputStream +import java.io.OutputStreamWriter +import java.lang.reflect.Type +import kotlin.math.ceil + +val INITIAL_GSON: Gson by lazy { + GsonBuilder() + .registerTypeAdapter( + object : TypeToken?>() {}.type, + MapDeserializerDoubleAsIntFix() + ) + .registerTypeAdapter(Int::class.java, IntJsonDeserializer()) + .registerTypeAdapter(String::class.java, StringJsonDeserializer()) + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + .disableHtmlEscaping() + .setPrettyPrinting() + .create() +} + +val GSON: Gson by lazy { + INITIAL_GSON.newBuilder() + .registerTypeAdapter(ExploreRule::class.java, ExploreRule.jsonDeserializer) + .registerTypeAdapter(SearchRule::class.java, SearchRule.jsonDeserializer) + .registerTypeAdapter(BookInfoRule::class.java, BookInfoRule.jsonDeserializer) + .registerTypeAdapter(TocRule::class.java, TocRule.jsonDeserializer) + .registerTypeAdapter(ContentRule::class.java, ContentRule.jsonDeserializer) + .registerTypeAdapter(ReviewRule::class.java, ReviewRule.jsonDeserializer) + .create() +} + +val GSONStrict: Gson by lazy { + GSON.newBuilder() + .setStrictness(Strictness.STRICT) + .create() +} + +inline fun genericType(): Type = object : TypeToken() {}.type + +inline fun Gson.fromJsonObject(json: String?): Result { + return kotlin.runCatching { + if (json == null) { + throw JsonSyntaxException("解析字符串为空") + } + fromJson(json, genericType()) as T + } +} + +inline fun Gson.fromJsonArray(json: String?): Result> { + return kotlin.runCatching { + if (json == null) { + throw JsonSyntaxException("解析字符串为空") + } + val type = TypeToken.getParameterized(List::class.java, T::class.java).type + val list = fromJson(json, type) as List + if (list.contains(null)) { + throw JsonSyntaxException( + "列表不能存在null元素,可能是json格式错误,通常为列表存在多余的逗号所致" + ) + } + @Suppress("UNCHECKED_CAST") + list as List + } +} + +inline fun Gson.fromJsonObject(inputStream: InputStream?): Result { + return kotlin.runCatching { + if (inputStream == null) { + throw JsonSyntaxException("解析流为空") + } + val reader = InputStreamReader(inputStream) + fromJson(reader, genericType()) as T + } +} + +inline fun Gson.fromJsonArray(inputStream: InputStream?): Result> { + return kotlin.runCatching { + if (inputStream == null) { + throw JsonSyntaxException("解析流为空") + } + val reader = InputStreamReader(inputStream) + val type = TypeToken.getParameterized(List::class.java, T::class.java).type + val list = fromJson(reader, type) as List + if (list.contains(null)) { + throw JsonSyntaxException( + "列表不能存在null元素,可能是json格式错误,通常为列表存在多余的逗号所致" + ) + } + @Suppress("UNCHECKED_CAST") + list as List + } +} + +fun Gson.writeToOutputStream(out: OutputStream, any: Any) { + val writer = JsonWriter(OutputStreamWriter(out, "UTF-8")) + writer.setIndent(" ") + if (any is List<*>) { + writer.beginArray() + any.forEach { + it?.let { + toJson(it, it::class.java, writer) + } + } + writer.endArray() + } else { + toJson(any, any::class.java, writer) + } + writer.close() +} + +/** + * + */ +class StringJsonDeserializer : JsonDeserializer { + + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext? + ): String? { + return when { + json.isJsonPrimitive -> json.asString + json.isJsonNull -> null + else -> json.toString() + } + } + +} + +/** + * int类型转化失败时跳过 + */ +class IntJsonDeserializer : JsonDeserializer { + + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): Int? { + return when { + json.isJsonPrimitive -> { + val prim = json.asJsonPrimitive + if (prim.isNumber) { + prim.asNumber.toInt() + } else { + null + } + } + + else -> null + } + } + +} + +/** + * 修复Int变为Double的问题 + */ +class MapDeserializerDoubleAsIntFix : + JsonDeserializer?> { + + @Throws(JsonParseException::class) + override fun deserialize( + jsonElement: JsonElement, + type: Type, + jsonDeserializationContext: JsonDeserializationContext + ): Map? { + @Suppress("unchecked_cast") + return read(jsonElement) as? Map + } + + fun read(json: JsonElement): Any? { + when { + json.isJsonArray -> { + val list: MutableList = ArrayList() + val arr = json.asJsonArray + for (anArr in arr) { + list.add(read(anArr)) + } + return list + } + + json.isJsonObject -> { + val map: MutableMap = + LinkedTreeMap() + val obj = json.asJsonObject + val entitySet = + obj.entrySet() + for ((key, value) in entitySet) { + map[key] = read(value) + } + return map + } + + json.isJsonPrimitive -> { + val prim = json.asJsonPrimitive + when { + prim.isBoolean -> { + return prim.asBoolean + } + + prim.isString -> { + return prim.asString + } + + prim.isNumber -> { + val num: Number = prim.asNumber + // here you can handle double int/long values + // and return any type you want + // this solution will transform 3.0 float to long values + return if (ceil(num.toDouble()) == num.toLong().toDouble()) { + num.toLong() + } else { + num.toDouble() + } + } + } + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/HandlerUtils.kt b/app/src/main/java/io/legado/app/utils/HandlerUtils.kt new file mode 100755 index 000000000..5da7da425 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/HandlerUtils.kt @@ -0,0 +1,50 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch + +/** This main looper cache avoids synchronization overhead when accessed repeatedly. */ +private val mainLooper: Looper = Looper.getMainLooper() + +private val mainThread: Thread = mainLooper.thread + +val isMainThread: Boolean get() = mainThread === Thread.currentThread() + +fun buildMainHandler(): Handler { + return if (SDK_INT >= 28) Handler.createAsync(mainLooper) else try { + Handler::class.java.getDeclaredConstructor( + Looper::class.java, + Handler.Callback::class.java, + Boolean::class.javaPrimitiveType // async + ).newInstance(mainLooper, null, true) + } catch (ignored: NoSuchMethodException) { + // Hidden constructor absent. Fall back to non-async constructor. + Handler(mainLooper) + } +} + +private val mainHandler by lazy { buildMainHandler() } + +fun runOnUI(function: () -> Unit) { + if (isMainThread) { + function() + } else { + mainHandler.post(function) + } +} + +fun CoroutineScope.runOnIO(function: () -> Unit) { + if (isMainThread) { + launch(IO) { + function() + } + } else { + function() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt new file mode 100755 index 000000000..5b67b4ad1 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt @@ -0,0 +1,71 @@ +package io.legado.app.utils + +import io.legado.app.model.analyzeRule.AnalyzeUrl +import java.net.URL +import java.util.regex.Pattern + +@Suppress("RegExpRedundantEscape") +object HtmlFormatter { + private val nbspRegex = "( )+".toRegex() + private val espRegex = "( | )".toRegex() + private val noPrintRegex = "( |‌|‍|\u2009|\u200C|\u200D)".toRegex() + private val wrapHtmlRegex = "]*>".toRegex() + private val commentRegex = "".toRegex() //注释 + private val notImgHtmlRegex = "])[^<>]*>".toRegex() + private val otherHtmlRegex = "])[^<>]*>".toRegex() + private val formatImagePattern = Pattern.compile( + "]*\\ssrc\\s*=\\s*['\"]([^'\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})['\"][^>]*>|]*\\s(?:data-src|src)\\s*=\\s*['\"]([^'\">]+)['\"][^>]*>|]*\\sdata-[^=>]*=\\s*['\"]([^'\">]*)['\"][^>]*>", + Pattern.CASE_INSENSITIVE + ) + private val indent1Regex = "\\s*\\n+\\s*".toRegex() + private val indent2Regex = "^[\\n\\s]+".toRegex() + private val lastRegex = "[\\n\\s]+$".toRegex() + + fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String { + html ?: return "" + return html.replace(nbspRegex, " ") + .replace(espRegex, " ") + .replace(noPrintRegex, "") + .replace(wrapHtmlRegex, "\n") + .replace(commentRegex, "") + .replace(otherRegex, "") + .replace(indent1Regex, "\n  ") + .replace(indent2Regex, "  ") + .replace(lastRegex, "") + } + + fun formatKeepImg(html: String?, redirectUrl: URL? = null): String { + html ?: return "" + val keepImgHtml = format(html, notImgHtmlRegex) + + //正则的“|”处于顶端而不处于()中时,具有类似||的熔断效果,故以此机制简化原来的代码 + val matcher = formatImagePattern.matcher(keepImgHtml) + var appendPos = 0 + val sb = StringBuilder() + while (matcher.find()) { + var param = "" + sb.append( + keepImgHtml.substring(appendPos, matcher.start()), "" + ) + appendPos = matcher.end() + } + if (appendPos < keepImgHtml.length) sb.append( + keepImgHtml.substring( + appendPos, + keepImgHtml.length + ) + ) + return sb.toString() + } +} diff --git a/app/src/main/java/io/legado/app/utils/ImageUtils.kt b/app/src/main/java/io/legado/app/utils/ImageUtils.kt new file mode 100755 index 000000000..5cae6f371 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ImageUtils.kt @@ -0,0 +1,74 @@ +package io.legado.app.utils + +import io.legado.app.constant.AppLog +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.RssSource +import java.io.ByteArrayInputStream +import java.io.InputStream + +/** + * 加密图片解密工具 + */ +object ImageUtils { + + /** + * @param isCover 根据这个执行书源中不同的解密规则 + * @return 解密失败返回Null 解密规则为空不处理 + */ + fun decode( + src: String, bytes: ByteArray, isCover: Boolean, + source: BaseSource?, book: Book? = null + ): ByteArray? { + val ruleJs = getRuleJs(source, isCover) + if (ruleJs.isNullOrBlank()) return bytes + //解密库hutool.crypto ByteArray|InputStream -> ByteArray + return kotlin.runCatching { + source?.evalJS(ruleJs) { + put("book", book) + put("result", bytes) + put("src", src) + } as ByteArray + }.onFailure { + AppLog.putDebug("${src}解密错误", it) + }.getOrNull() + } + + fun decode( + src: String, inputStream: InputStream, isCover: Boolean, + source: BaseSource?, book: Book? = null + ): InputStream? { + val ruleJs = getRuleJs(source, isCover) + if (ruleJs.isNullOrBlank()) return inputStream + //解密库hutool.crypto ByteArray|InputStream -> ByteArray + return kotlin.runCatching { + val bytes = source?.evalJS(ruleJs) { + put("book", book) + put("result", inputStream) + put("src", src) + } as ByteArray + ByteArrayInputStream(bytes) + }.onFailure { + AppLog.putDebug("${src}解密错误", it) + }.getOrNull() + } + + fun skipDecode(source: BaseSource?, isCover: Boolean): Boolean { + return getRuleJs(source, isCover).isNullOrBlank() + } + + private fun getRuleJs( + source: BaseSource?, isCover: Boolean + ): String? { + return when (source) { + is BookSource -> + if (isCover) source.coverDecodeJs + else source.getContentRule().imageDecode + + is RssSource -> source.coverDecodeJs + else -> null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/InputStreamExtensions.kt b/app/src/main/java/io/legado/app/utils/InputStreamExtensions.kt new file mode 100755 index 000000000..e36814f8e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/InputStreamExtensions.kt @@ -0,0 +1,25 @@ +package io.legado.app.utils + +import java.io.InputStream +import java.util.* + +fun InputStream?.isJson(): Boolean { + this ?: return false + this.use { + val byteArray = ByteArray(128) + it.read(byteArray) + val a = String(byteArray).trim() + it.skip(it.available() - 128L) + it.read(byteArray) + val b = String(byteArray).trim() + return (a + b).isJson() + } +} + +fun InputStream?.contains(str: String): Boolean { + this ?: return false + this.use { + val scanner = Scanner(it) + return scanner.findWithinHorizon(str, 0) != null + } +} diff --git a/app/src/main/java/io/legado/app/utils/IntentExtensions.kt b/app/src/main/java/io/legado/app/utils/IntentExtensions.kt new file mode 100755 index 000000000..3edc732bd --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/IntentExtensions.kt @@ -0,0 +1,21 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.content.Intent + +fun Intent.putJson(key: String, any: Any?) { + any?.let { + putExtra(key, GSON.toJson(any)) + } +} + +inline fun Intent.getJsonObject(key: String): T? { + val value = getStringExtra(key) + return GSON.fromJsonObject(value).getOrNull() +} + +inline fun Intent.getJsonArray(key: String): List? { + val value = getStringExtra(key) + return GSON.fromJsonArray(value).getOrNull() +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/IntentType.kt b/app/src/main/java/io/legado/app/utils/IntentType.kt new file mode 100755 index 000000000..90385292d --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/IntentType.kt @@ -0,0 +1,31 @@ +package io.legado.app.utils + +import android.net.Uri +import java.io.File + +object IntentType { + + fun from(uri: Uri): String { + return from(uri.toString()) + } + + fun from(file: File): String { + return from(file.absolutePath) + } + + fun from(path: String?): String { + val suffix = path + ?.substringAfterLast(File.separator) + ?.substringAfterLast(".", "") + ?.lowercase() + return when (suffix) { + "m4a", "mp3", "mid", "xmf", "ogg", "wav" -> "video/*" + "3gp", "mp4" -> "audio/*" + "jpg", "gif", "png", "jpeg", "bmp" -> "image/*" + "", "txt", "json", "log" -> "text/plain" + "apk" -> "application/vnd.android.package-archive" + else -> "*/*" + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/JsURL.kt b/app/src/main/java/io/legado/app/utils/JsURL.kt new file mode 100755 index 000000000..7596765e8 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/JsURL.kt @@ -0,0 +1,44 @@ +package io.legado.app.utils + +import androidx.annotation.Keep +import java.net.URL +import java.net.URLDecoder + +@Keep +@Suppress("MemberVisibilityCanBePrivate") +class JsURL(url: String, baseUrl: String? = null) { + + val searchParams: Map? + val host: String + val origin: String + val pathname: String + + init { + val mUrl = if (!baseUrl.isNullOrEmpty()) { + val base = URL(baseUrl) + URL(base, url) + } else { + URL(url) + } + host = mUrl.host + origin = if (mUrl.port > 0) { + "${mUrl.protocol}://$host:${mUrl.port}" + } else { + "${mUrl.protocol}://$host" + } + pathname = mUrl.path + val query = mUrl.query + searchParams = query?.let { _ -> + val map = hashMapOf() + query.split("&").forEach { + val x = it.split("=", limit = 2) + if (x.size == 2) { + map[x[0]] = URLDecoder.decode(x[1], "utf-8") + } + } + map + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/JsonExtensions.kt b/app/src/main/java/io/legado/app/utils/JsonExtensions.kt new file mode 100755 index 000000000..1e9a0289d --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/JsonExtensions.kt @@ -0,0 +1,20 @@ +package io.legado.app.utils + +import com.jayway.jsonpath.* + +val jsonPath: ParseContext by lazy { + JsonPath.using( + Configuration.builder() + .options(Option.SUPPRESS_EXCEPTIONS) + .build() + ) +} + +fun ReadContext.readString(path: String): String? = this.read(path, String::class.java) + +fun ReadContext.readBool(path: String): Boolean? = this.read(path, Boolean::class.java) + +fun ReadContext.readInt(path: String): Int? = this.read(path, Int::class.java) + +fun ReadContext.readLong(path: String): Long? = this.read(path, Long::class.java) + diff --git a/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt b/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt new file mode 100755 index 000000000..b86ca225d --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/JsoupExtensions.kt @@ -0,0 +1,78 @@ +package io.legado.app.utils + +import org.jsoup.internal.StringUtil +import org.jsoup.nodes.CDataNode +import org.jsoup.nodes.Element +import org.jsoup.nodes.Node +import org.jsoup.nodes.TextNode +import org.jsoup.select.Elements +import org.jsoup.select.NodeTraversor +import org.jsoup.select.NodeVisitor + + +fun Element.textArray(): Array { + val sb = StringUtil.borrowBuilder() + NodeTraversor.traverse(object : NodeVisitor { + override fun head(node: Node, depth: Int) { + if (node is TextNode) { + appendNormalisedText(sb, node) + } else if (node is Element) { + if (sb.isNotEmpty() && + (node.isBlock || node.tag().name == "br") && + !lastCharIsWhitespace(sb) + ) sb.append("\n") + } + } + + override fun tail(node: Node, depth: Int) { + if (node is Element) { + if (node.isBlock && node.nextSibling() is TextNode + && !lastCharIsWhitespace(sb) + ) { + sb.append("\n") + } + } + } + }, this) + val text = StringUtil.releaseBuilder(sb).trim { it <= ' ' } + return text.splitNotBlank("\n") +} + +fun Element.findNS(tag: String, namespace: HashSet): Elements { + return select("*|$tag").filter { el -> + namespace.contains(el.tagName().substringBefore(":")) + }.toElements() +} + +fun Element.findNSPrefix(namespaceURI: String): HashSet { + return select("[^xmlns:]").map { element -> + element.attributes().filter { it.value == namespaceURI }.map { it.key.substring(6) } + }.flatten().toHashSet() +} + +fun List.toElements() = Elements(this) + +private fun appendNormalisedText(sb: StringBuilder, textNode: TextNode) { + val text = textNode.wholeText + if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode) + sb.append(text) + else StringUtil.appendNormalisedWhitespace(sb, text, lastCharIsWhitespace(sb)) +} + +private fun preserveWhitespace(node: Node?): Boolean { + if (node is Element) { + var el = node as Element? + var i = 0 + do { + if (el!!.tag().preserveWhitespace()) return true + el = el.parent() + i++ + } while (i < 6 && el != null) + } + return false +} + +private fun lastCharIsWhitespace(sb: java.lang.StringBuilder): Boolean { + return sb.isNotEmpty() && sb[sb.length - 1] == ' ' +} + diff --git a/app/src/main/java/io/legado/app/utils/LogUtils.kt b/app/src/main/java/io/legado/app/utils/LogUtils.kt new file mode 100755 index 000000000..3f807396d --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/LogUtils.kt @@ -0,0 +1,144 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.webkit.WebSettings +import io.legado.app.BuildConfig +import io.legado.app.constant.AppConst +import io.legado.app.constant.AppLog +import io.legado.app.help.config.AppConfig +import io.legado.app.help.globalExecutor +import splitties.init.appCtx +import java.text.SimpleDateFormat +import java.util.Date +import java.util.logging.FileHandler +import java.util.logging.Level +import java.util.logging.LogRecord +import java.util.logging.Logger +import kotlin.time.Duration.Companion.days + +@SuppressLint("SimpleDateFormat") +@Suppress("unused") +object LogUtils { + const val TIME_PATTERN = "yy-MM-dd HH:mm:ss.SSS" + val logTimeFormat by lazy { SimpleDateFormat(TIME_PATTERN) } + + fun init(context: Context) { + fileHandler = createFileHandler(context)?.also { + logger.addHandler(it) + } + } + + @JvmStatic + fun d(tag: String, msg: String) { + logger.log(Level.INFO, "$tag $msg") + } + + inline fun d(tag: String, lazyMsg: () -> String) { + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "$tag ${lazyMsg()}") + } + } + + @JvmStatic + fun e(tag: String, msg: String) { + logger.log(Level.WARNING, "$tag $msg") + } + + val logger: Logger by lazy { + Logger.getLogger("Legado") + } + + private var fileHandler: FileHandler? = null + + private fun createFileHandler(context: Context): FileHandler? { + try { + val root = context.externalCacheDir ?: return null + val logFolder = FileUtils.createFolderIfNotExist(root, "logs") + globalExecutor.execute { + val expiredTime = System.currentTimeMillis() - 7.days.inWholeMilliseconds + logFolder.listFiles()?.forEach { + if (it.lastModified() < expiredTime || it.name.endsWith(".lck")) { + it.delete() + } + } + } + val date = getCurrentDateStr(TIME_PATTERN).replace(" ", "_").replace(":", "-") + val logPath = FileUtils.getPath(root = logFolder, "appLog-$date.txt") + return AsyncFileHandler(logPath).apply { + formatter = object : java.util.logging.Formatter() { + override fun format(record: LogRecord): String { + // 设置文件输出格式 + return getCurrentDateStr(TIME_PATTERN) + ": " + record.message + "\n" + } + } + level = if (AppConfig.recordLog) { + Level.INFO + } else { + Level.OFF + } + } + } catch (e: Exception) { + e.printStackTrace() + AppLog.putNotSave("创建fileHandler出错\n$e", e) + return null + } + } + + fun upLevel() { + val level = if (AppConfig.recordLog) { + Level.INFO + } else { + Level.OFF + } + fileHandler?.level = level + } + + /** + * 获取当前时间 + */ + @SuppressLint("SimpleDateFormat") + fun getCurrentDateStr(pattern: String): String { + val date = Date() + val sdf = SimpleDateFormat(pattern) + return sdf.format(date) + } + + fun logDeviceInfo() { + d("DeviceInfo") { + buildString { + kotlin.runCatching { + //获取系统信息 + append("MANUFACTURER=").append(Build.MANUFACTURER).append("\n") + append("BRAND=").append(Build.BRAND).append("\n") + append("MODEL=").append(Build.MODEL).append("\n") + append("SDK_INT=").append(Build.VERSION.SDK_INT).append("\n") + append("RELEASE=").append(Build.VERSION.RELEASE).append("\n") + val userAgent = try { + WebSettings.getDefaultUserAgent(appCtx) + } catch (e: Throwable) { + e.toString() + } + append("WebViewUserAgent=").append(userAgent).append("\n") + append("packageName=").append(appCtx.packageName).append("\n") + append("heapSize=").append(Runtime.getRuntime().maxMemory()).append("\n") + //获取app版本信息 + AppConst.appInfo.let { + append("versionName=").append(it.versionName).append("\n") + append("versionCode=").append(it.versionCode).append("\n") + } + } + } + } + } + +} + +fun Throwable.printOnDebug() { + if (BuildConfig.DEBUG) { + printStackTrace() + } +} diff --git a/app/src/main/java/io/legado/app/utils/MD5Utils.kt b/app/src/main/java/io/legado/app/utils/MD5Utils.kt new file mode 100755 index 000000000..261626aa8 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/MD5Utils.kt @@ -0,0 +1,34 @@ +package io.legado.app.utils + +import cn.hutool.crypto.digest.DigestUtil +import cn.hutool.crypto.digest.Digester +import java.io.InputStream +import kotlin.concurrent.getOrSet + +/** + * 将字符串转化为MD5 + */ +@Suppress("unused") +object MD5Utils { + + private val threadLocal = ThreadLocal() + + private val MD5Digester + get() = threadLocal.getOrSet { + DigestUtil.digester("MD5") + } + + fun md5Encode(str: String?): String { + return MD5Digester.digestHex(str) + } + + fun md5Encode(inputStream: InputStream): String { + return MD5Digester.digestHex(inputStream) + } + + fun md5Encode16(str: String): String { + var reStr = md5Encode(str) + reStr = reStr.substring(8, 24) + return reStr + } +} diff --git a/app/src/main/java/io/legado/app/utils/MapExtensions.kt b/app/src/main/java/io/legado/app/utils/MapExtensions.kt new file mode 100755 index 000000000..d035b2a73 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/MapExtensions.kt @@ -0,0 +1,32 @@ +package io.legado.app.utils + +fun HashMap.has(key: String, ignoreCase: Boolean = false): Boolean { + for (item in this) { + if (key.equals(item.key, ignoreCase)) { + return true + } + } + return false +} + +fun HashMap.get(key: String, ignoreCase: Boolean = false): T? { + for (item in this) { + if (key.equals(item.key, ignoreCase)) { + return item.value + } + } + return null +} + +inline fun MutableMap.getOrPutLimit(key: K, maxSize: Int, defaultValue: () -> V): V { + var value = get(key) + if (containsKey(key)) { + @Suppress("UNCHECKED_CAST") + return value as V + } + value = defaultValue() + if (size < maxSize) { + put(key, value) + } + return value +} diff --git a/app/src/main/java/io/legado/app/utils/MenuExtensions.kt b/app/src/main/java/io/legado/app/utils/MenuExtensions.kt new file mode 100755 index 000000000..39e552f0e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/MenuExtensions.kt @@ -0,0 +1,113 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageButton +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.appcompat.view.menu.SubMenuBuilder +import androidx.core.view.forEach +import io.legado.app.R +import io.legado.app.constant.Theme +import io.legado.app.lib.theme.primaryTextColor +import java.lang.reflect.Method + +@SuppressLint("RestrictedApi") +@Suppress("UsePropertyAccessSyntax") +fun Menu.applyTint(context: Context, theme: Theme = Theme.Auto): Menu = this.let { menu -> + if (menu is MenuBuilder) { + menu.setOptionalIconsVisible(true) + } + val defaultTextColor = context.getCompatColor(R.color.primaryText) + val tintColor = MenuExtensions.getMenuColor(context, theme) + menu.forEach { item -> + (item as MenuItemImpl).let { impl -> + //overflow:展开的item + impl.icon?.setTintMutate( + if (impl.requiresOverflow()) defaultTextColor else tintColor + ) + } + } + return menu +} + +@SuppressLint("RestrictedApi") +fun Menu.applyOpenTint(context: Context) { + //展开菜单显示图标 + if (this.javaClass.simpleName.equals("MenuBuilder", ignoreCase = true)) { + val defaultTextColor = context.getCompatColor(R.color.primaryText) + kotlin.runCatching { + var method: Method = + this.javaClass.getDeclaredMethod("setOptionalIconsVisible", java.lang.Boolean.TYPE) + method.isAccessible = true + method.invoke(this, true) + method = this.javaClass.getDeclaredMethod("getNonActionItems") + val menuItems = method.invoke(this) + if (menuItems is ArrayList<*>) { + for (menuItem in menuItems) { + if (menuItem is MenuItem) { + menuItem.icon?.setTintMutate(defaultTextColor) + } + } + } + } + } else if (this.javaClass.simpleName.equals("SubMenuBuilder", ignoreCase = true)) { + val defaultTextColor = context.getCompatColor(R.color.primaryText) + (this as? SubMenuBuilder)?.forEach { item: MenuItem -> + item.icon?.setTintMutate(defaultTextColor) + } + } +} + +fun Menu.iconItemOnLongClick(id: Int, function: (view: View) -> Unit) { + findItem(id)?.let { item -> + item.setActionView(R.layout.view_action_button) + item.actionView?.run { + contentDescription = item.title + findViewById(R.id.item).setImageDrawable(item.icon) + setOnLongClickListener { + function.invoke(this) + true + } + setOnClickListener { + performIdentifierAction(id, 0) + } + } + } +} + +@SuppressLint("RestrictedApi") +inline fun Menu.transaction(block: (Menu) -> Unit) { + val menuBuilder = this as? MenuBuilder + menuBuilder?.stopDispatchingItemsChanged() + try { + block(this) + } finally { + menuBuilder?.startDispatchingItemsChanged() + } +} + +object MenuExtensions { + + fun getMenuColor( + context: Context, + theme: Theme = Theme.Auto, + requiresOverflow: Boolean = false + ): Int { + val defaultTextColor = context.getCompatColor(R.color.primaryText) + if (requiresOverflow) + return defaultTextColor + val primaryTextColor = context.primaryTextColor + return when (theme) { + Theme.Dark -> context.getCompatColor(R.color.md_white_1000) + Theme.Light -> context.getCompatColor(R.color.md_black_1000) + else -> primaryTextColor + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/MenuItemExtensions.kt b/app/src/main/java/io/legado/app/utils/MenuItemExtensions.kt new file mode 100755 index 000000000..fe83d6432 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/MenuItemExtensions.kt @@ -0,0 +1,11 @@ +package io.legado.app.utils + +import android.view.MenuItem +import android.widget.ImageButton +import androidx.annotation.DrawableRes +import io.legado.app.R + +fun MenuItem.setIconCompat(@DrawableRes iconRes: Int) { + setIcon(iconRes) + actionView?.findViewById(R.id.item)?.setImageDrawable(icon) +} diff --git a/app/src/main/java/io/legado/app/utils/MutableLiveDataExtensions.kt b/app/src/main/java/io/legado/app/utils/MutableLiveDataExtensions.kt new file mode 100755 index 000000000..c4fc6be6e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/MutableLiveDataExtensions.kt @@ -0,0 +1,13 @@ +package io.legado.app.utils + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.MutableLiveData + +private val mainHandler by lazy { Handler(Looper.getMainLooper()) } + +fun MutableLiveData.sendValue(value: T) { + mainHandler.post { + this@sendValue.value = value + } +} diff --git a/app/src/main/java/io/legado/app/utils/NavigationViewUtils.kt b/app/src/main/java/io/legado/app/utils/NavigationViewUtils.kt new file mode 100755 index 000000000..6f68d090f --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/NavigationViewUtils.kt @@ -0,0 +1,42 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.content.res.ColorStateList +import androidx.annotation.ColorInt +import com.google.android.material.internal.NavigationMenuView +import com.google.android.material.navigation.NavigationView + +fun NavigationView.setItemIconColors( + @ColorInt normalColor: Int, + @ColorInt selectedColor: Int +) { + val iconSl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_checked), + intArrayOf(android.R.attr.state_checked) + ), + intArrayOf(normalColor, selectedColor) + ) + itemIconTintList = iconSl +} + +fun NavigationView.setItemTextColors( + @ColorInt normalColor: Int, + @ColorInt selectedColor: Int +) { + val textSl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_checked), + intArrayOf(android.R.attr.state_checked) + ), + intArrayOf(normalColor, selectedColor) + ) + itemTextColor = textSl +} + +fun NavigationView.disableScrollbar() { + val navigationMenuView = getChildAt(0) as? NavigationMenuView + navigationMenuView?.isVerticalScrollBarEnabled = false +} + diff --git a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt new file mode 100755 index 000000000..0be4bda13 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -0,0 +1,298 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import cn.hutool.core.lang.Validator +import io.legado.app.constant.AppLog +import okhttp3.internal.publicsuffix.PublicSuffixDatabase +import splitties.systemservices.connectivityManager +import java.net.InetAddress +import java.net.NetworkInterface +import java.net.SocketException +import java.net.URL +import java.util.BitSet +import java.util.Enumeration + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object NetworkUtils { + + /** + * 判断是否联网 + */ + @SuppressLint("ObsoleteSdkInt") + @Suppress("DEPRECATION") + fun isAvailable(): Boolean { + if (Build.VERSION.SDK_INT < 23) { + val mWiFiNetworkInfo = connectivityManager.activeNetworkInfo + if (mWiFiNetworkInfo != null) { + // WIFI + return mWiFiNetworkInfo.type == ConnectivityManager.TYPE_WIFI || + // 移动数据 + mWiFiNetworkInfo.type == ConnectivityManager.TYPE_MOBILE || + // 以太网 + mWiFiNetworkInfo.type == ConnectivityManager.TYPE_ETHERNET || + // VPN + mWiFiNetworkInfo.type == ConnectivityManager.TYPE_VPN + } + } else { + val network = connectivityManager.activeNetwork + if (network != null) { + val nc = connectivityManager.getNetworkCapabilities(network) + if (nc != null) { + // WIFI + return nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + // 移动数据 + nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + // 以太网 + nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || + // VPN + nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN) + } + } + } + return false + } + + private val notNeedEncodingQuery: BitSet by lazy { + val bitSet = BitSet(256) + for (i in 'a'.code..'z'.code) { + bitSet.set(i) + } + for (i in 'A'.code..'Z'.code) { + bitSet.set(i) + } + for (i in '0'.code..'9'.code) { + bitSet.set(i) + } + for (char in "!$&()*+,-./:;=?@[\\]^_`{|}~") { + bitSet.set(char.code) + } + return@lazy bitSet + } + + private val notNeedEncodingForm: BitSet by lazy { + val bitSet = BitSet(256) + for (i in 'a'.code..'z'.code) { + bitSet.set(i) + } + for (i in 'A'.code..'Z'.code) { + bitSet.set(i) + } + for (i in '0'.code..'9'.code) { + bitSet.set(i) + } + for (char in "*-._") { + bitSet.set(char.code) + } + return@lazy bitSet + } + + /** + * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' + * 0-9a-zA-Z保留

    + * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留 + * 其他字符转成%XX的格式,X是16进制的大写字符,范围是[0-9A-F] + */ + fun encodedQuery(str: String): Boolean { + var needEncode = false + var i = 0 + while (i < str.length) { + val c = str[i] + if (notNeedEncodingQuery.get(c.code)) { + i++ + continue + } + if (c == '%' && i + 2 < str.length) { + // 判断是否符合urlEncode规范 + val c1 = str[++i] + val c2 = str[++i] + if (isDigit16Char(c1) && isDigit16Char(c2)) { + i++ + continue + } + } + // 其他字符,肯定需要urlEncode + needEncode = true + break + } + + return !needEncode + } + + fun encodedForm(str: String): Boolean { + var needEncode = false + var i = 0 + while (i < str.length) { + val c = str[i] + if (notNeedEncodingForm.get(c.code)) { + i++ + continue + } + if (c == '%' && i + 2 < str.length) { + // 判断是否符合urlEncode规范 + val c1 = str[++i] + val c2 = str[++i] + if (isDigit16Char(c1) && isDigit16Char(c2)) { + i++ + continue + } + } + // 其他字符,肯定需要urlEncode + needEncode = true + break + } + + return !needEncode + } + + /** + * 判断c是否是16进制的字符 + */ + private fun isDigit16Char(c: Char): Boolean { + return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f' + } + + /** + * 获取绝对地址 + */ + fun getAbsoluteURL(baseURL: String?, relativePath: String): String { + if (baseURL.isNullOrEmpty()) return relativePath.trim() + var absoluteUrl: URL? = null + try { + absoluteUrl = URL(baseURL.substringBefore(",")) + } catch (e: Exception) { + e.printOnDebug() + } + return getAbsoluteURL(absoluteUrl, relativePath) + } + + /** + * 获取绝对地址 + */ + fun getAbsoluteURL(baseURL: URL?, relativePath: String): String { + val relativePathTrim = relativePath.trim() + if (baseURL == null) return relativePathTrim + if (relativePathTrim.isAbsUrl()) return relativePathTrim + if (relativePathTrim.isDataUrl()) return relativePathTrim + if (relativePathTrim.startsWith("javascript")) return "" + var relativeUrl = relativePathTrim + try { + val parseUrl = URL(baseURL, relativePath) + relativeUrl = parseUrl.toString() + return relativeUrl + } catch (e: Exception) { + AppLog.put("网址拼接出错\n${e.localizedMessage}", e) + } + return relativeUrl + } + + fun getBaseUrl(url: String?): String? { + url ?: return null + if (url.startsWith("http://", true) + || url.startsWith("https://", true) + ) { + val index = url.indexOf("/", 9) + return if (index == -1) { + url + } else url.substring(0, index) + } + return null + } + + /** + * 获取域名,供cookie保存和读取,处理失败返回传入的url + * http://1.2.3.4 => 1.2.3.4 + * https://www.example.com => example.com + * http://www.biquge.com.cn => biquge.com.cn + * http://www.content.example.com => example.com + */ + fun getSubDomain(url: String): String { + val baseUrl = getBaseUrl(url) ?: return url + return kotlin.runCatching { + val mURL = URL(baseUrl) + val host: String = mURL.host + //mURL.scheme https/http + //判断是否为ip + if (isIPAddress(host)) return host + //PublicSuffixDatabase处理域名 + PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) ?: host + }.getOrDefault(baseUrl) + } + + fun getSubDomainOrNull(url: String): String? { + val baseUrl = getBaseUrl(url) ?: return null + return kotlin.runCatching { + val mURL = URL(baseUrl) + val host: String = mURL.host + //mURL.scheme https/http + //判断是否为ip + if (isIPAddress(host)) return host + //PublicSuffixDatabase处理域名 + PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) ?: host + }.getOrDefault(null) + } + + fun getDomain(url: String): String { + val baseUrl = getBaseUrl(url) ?: return url + return kotlin.runCatching { + URL(baseUrl).host + }.getOrDefault(baseUrl) + } + + /** + * Get local Ip address. + */ + fun getLocalIPAddress(): List { + val enumeration: Enumeration + try { + enumeration = NetworkInterface.getNetworkInterfaces() + } catch (e: SocketException) { + e.printOnDebug() + return emptyList() + } + + val addressList = mutableListOf() + + while (enumeration.hasMoreElements()) { + val nif = enumeration.nextElement() + val addresses = nif.inetAddresses ?: continue + while (addresses.hasMoreElements()) { + val address = addresses.nextElement() + if (!address.isLoopbackAddress && isIPv4Address(address.hostAddress)) { + addressList.add(address) + } + } + } + return addressList + } + + /** + * Check if valid IPV4 address. + * + * @param input the address string to check for validity. + * @return True if the input parameter is a valid IPv4 address. + */ + fun isIPv4Address(input: String?): Boolean { + return input != null && input.isNotEmpty() + && input[0] in '1'..'9' + && input.count { it == '.' } == 3 + && Validator.isIpv4(input) + } + + /** + * Check if valid IPV6 address. + */ + fun isIPv6Address(input: String?): Boolean { + return input != null && input.contains(":") && Validator.isIpv6(input) + } + + /** + * Check if valid IP address. + */ + fun isIPAddress(input: String?): Boolean { + return isIPv4Address(input) || isIPv6Address(input) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/PaintExtensions.kt b/app/src/main/java/io/legado/app/utils/PaintExtensions.kt new file mode 100755 index 000000000..ae4505aa4 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/PaintExtensions.kt @@ -0,0 +1,27 @@ +package io.legado.app.utils + +import android.os.Build +import android.text.TextPaint + +val TextPaint.textHeight: Float + get() = fontMetrics.run { descent - ascent + leading } + +fun TextPaint.getTextWidthsCompat(text: String, widths: FloatArray) { + getTextWidths(text, widths) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + val letterSpacing = letterSpacing * textSize + val letterSpacingHalf = letterSpacing * 0.5f + for (i in widths.indices) { + if (widths[i] > 0) { + widths[i] += letterSpacingHalf + break + } + } + for (i in text.lastIndex downTo 0) { + if (widths[i] > 0) { + widths[i] += letterSpacingHalf + break + } + } + } +} diff --git a/app/src/main/java/io/legado/app/utils/ParcelFileDescriptorChannel.kt b/app/src/main/java/io/legado/app/utils/ParcelFileDescriptorChannel.kt new file mode 100755 index 000000000..bd0a5ed22 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ParcelFileDescriptorChannel.kt @@ -0,0 +1,80 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.os.ParcelFileDescriptor +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.SeekableByteChannel + +@Suppress("unused") +@SuppressLint("NewApi") +class ParcelFileDescriptorChannel(private val pfd: ParcelFileDescriptor) : SeekableByteChannel { + @Throws(IOException::class) + override fun read(dst: ByteBuffer): Int { + return try { + Os.read(pfd.fileDescriptor, dst) + } catch (e: ErrnoException) { + throw RuntimeException(e) + } + } + + @Throws(IOException::class) + override fun write(src: ByteBuffer): Int { + return try { + Os.write(pfd.fileDescriptor, src) + } catch (e: ErrnoException) { + throw RuntimeException(e) + } + } + + @Throws(IOException::class) + override fun position(): Long { + return try { + Os.lseek(pfd.fileDescriptor, 0, OsConstants.SEEK_CUR) + } catch (e: ErrnoException) { + throw RuntimeException(e) + } + } + + @Throws(IOException::class) + override fun position(newPosition: Long): SeekableByteChannel { + try { + Os.lseek(pfd.fileDescriptor, newPosition, OsConstants.SEEK_SET) + } catch (e: ErrnoException) { + throw RuntimeException(e) + } + return this + } + + @Throws(IOException::class) + override fun size(): Long { + return try { + Os.fstat(pfd.fileDescriptor).st_size + } catch (e: ErrnoException) { + throw RuntimeException(e) + } + } + + @Throws(IOException::class) + override fun truncate(newSize: Long): SeekableByteChannel { + require(!(newSize < 0L || newSize > Int.MAX_VALUE)) { "Size has to be in range 0.. " + Int.MAX_VALUE } + try { + Os.ftruncate(pfd.fileDescriptor, newSize) + } catch (e: ErrnoException) { + throw RuntimeException(e) + } + return this + } + + override fun isOpen(): Boolean { + return pfd.fileDescriptor.valid() + } + + @Throws(IOException::class) + override fun close() { + pfd.close() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/PreferencesExtensions.kt b/app/src/main/java/io/legado/app/utils/PreferencesExtensions.kt new file mode 100755 index 000000000..9e128e03a --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/PreferencesExtensions.kt @@ -0,0 +1,131 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import splitties.init.appCtx +import java.io.File + +/** + * 获取自定义路径的SharedPreferences, 用反射生成 SharedPreferences + * @param dir 目录路径 + * @param fileName 文件名,不需要 '.xml' 后缀 + * @return SharedPreferences + */ +@SuppressLint("DiscouragedPrivateApi") +fun Context.getSharedPreferences( + dir: String, + fileName: String +): SharedPreferences? { + try { + // 获取 ContextWrapper对象中的mBase变量。该变量保存了 ContextImpl 对象 + val fieldMBase = ContextWrapper::class.java.getDeclaredField("mBase") + fieldMBase.isAccessible = true + // 获取 mBase变量 + val objMBase = fieldMBase.get(this) + // 获取 ContextImpl。mPreferencesDir变量,该变量保存了数据文件的保存路径 + val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir") + fieldMPreferencesDir.isAccessible = true + // 创建自定义路径 + val file = File(dir) + // 修改mPreferencesDir变量的值 + fieldMPreferencesDir.set(objMBase, file) + // 返回修改路径以后的 SharedPreferences :%FILE_PATH%/%fileName%.xml + return getSharedPreferences(fileName, Activity.MODE_PRIVATE) + } catch (e: NoSuchFieldException) { + e.printOnDebug() + } catch (e: IllegalArgumentException) { + e.printOnDebug() + } catch (e: IllegalAccessException) { + e.printOnDebug() + } + return null +} + +fun SharedPreferences.getString(key: String): String? { + return getString(key, null) +} + +fun SharedPreferences.putString(key: String, value: String) { + edit { + putString(key, value) + } +} + +fun SharedPreferences.getBoolean(key: String): Boolean { + return getBoolean(key, false) +} + +fun SharedPreferences.putBoolean(key: String, value: Boolean) { + edit { + putBoolean(key, value) + } +} + +fun SharedPreferences.getInt(key: String): Int { + return getInt(key, 0) +} + +fun SharedPreferences.putInt(key: String, value: Int) { + edit { + putInt(key, value) + } +} + +fun SharedPreferences.getLong(key: String): Long { + return getLong(key, 0) +} + +fun SharedPreferences.putLong(key: String, value: Long) { + edit { + putLong(key, value) + } +} + +fun SharedPreferences.getFloat(key: String): Float { + return getFloat(key, 0f) +} + +fun SharedPreferences.putFloat(key: String, value: Float) { + edit { + putFloat(key, value) + } +} + +fun SharedPreferences.remove(key: String) { + edit { + remove(key) + } +} + +fun LifecycleOwner.observeSharedPreferences( + prefs: SharedPreferences = appCtx.defaultSharedPreferences, + l: SharedPreferences.OnSharedPreferenceChangeListener +) { + val observer = object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + prefs.registerOnSharedPreferenceChangeListener(l) + } + + override fun onDestroy(owner: LifecycleOwner) { + prefs.unregisterOnSharedPreferenceChangeListener(l) + lifecycle.removeObserver(this) + } + + override fun onPause(owner: LifecycleOwner) { + prefs.unregisterOnSharedPreferenceChangeListener(l) + } + + override fun onResume(owner: LifecycleOwner) { + prefs.registerOnSharedPreferenceChangeListener(l) + } + } + lifecycle.addObserver(observer) +} diff --git a/app/src/main/java/io/legado/app/utils/QRCodeUtils.kt b/app/src/main/java/io/legado/app/utils/QRCodeUtils.kt new file mode 100755 index 000000000..a51eb1315 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/QRCodeUtils.kt @@ -0,0 +1,479 @@ +package io.legado.app.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.text.TextPaint +import android.text.TextUtils +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import com.google.zxing.BarcodeFormat +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.EncodeHintType +import com.google.zxing.LuminanceSource +import com.google.zxing.MultiFormatReader +import com.google.zxing.MultiFormatWriter +import com.google.zxing.RGBLuminanceSource +import com.google.zxing.Result +import com.google.zxing.WriterException +import com.google.zxing.common.GlobalHistogramBinarizer +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.king.zxing.DecodeFormatManager +import java.util.EnumMap +import kotlin.math.max + + +@Suppress("MemberVisibilityCanBePrivate", "unused") +object QRCodeUtils { + + const val DEFAULT_REQ_WIDTH = 480 + const val DEFAULT_REQ_HEIGHT = 640 + + /** + * 生成二维码 + * @param content 二维码的内容 + * @param heightPix 二维码的高 + * @param logo 二维码中间的logo + * @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3 + * @param errorCorrectionLevel + */ + fun createQRCode( + content: String, + heightPix: Int = DEFAULT_REQ_HEIGHT, + logo: Bitmap? = null, + @FloatRange(from = 0.0, to = 1.0) ratio: Float = 0.2f, + errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H + ): Bitmap? { + //配置参数 + val hints: MutableMap = EnumMap(EncodeHintType::class.java) + hints[EncodeHintType.CHARACTER_SET] = "utf-8" + //容错级别 + hints[EncodeHintType.ERROR_CORRECTION] = errorCorrectionLevel + //设置空白边距的宽度 + hints[EncodeHintType.MARGIN] = 1 //default is 4 + return createQRCode(content, heightPix, logo, ratio, hints) + } + + /** + * 生成二维码 + * @param content 二维码的内容 + * @param heightPix 二维码的高 + * @param logo 二维码中间的logo + * @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3 + * @param hints + * @param codeColor 二维码的颜色 + * @return + */ + fun createQRCode( + content: String?, + heightPix: Int, + logo: Bitmap?, + @FloatRange(from = 0.0, to = 1.0) ratio: Float = 0.2f, + hints: Map, + codeColor: Int = Color.BLACK + ): Bitmap? { + try { + // 图像数据转换,使用了矩阵转换 + val bitMatrix = + QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, heightPix, heightPix, hints) + val pixels = IntArray(heightPix * heightPix) + // 下面这里按照二维码的算法,逐个生成二维码的图片, + // 两个for循环是图片横列扫描的结果 + for (y in 0 until heightPix) { + for (x in 0 until heightPix) { + if (bitMatrix[x, y]) { + pixels[y * heightPix + x] = codeColor + } else { + pixels[y * heightPix + x] = Color.WHITE + } + } + } + + // 生成二维码图片的格式 + var bitmap: Bitmap? = Bitmap.createBitmap(heightPix, heightPix, Bitmap.Config.ARGB_8888) + bitmap!!.setPixels(pixels, 0, heightPix, 0, 0, heightPix, heightPix) + if (logo != null) { + bitmap = addLogo(bitmap, logo, ratio) + } + return bitmap + } catch (e: WriterException) { + e.printOnDebug() + } + return null + } + + /** + * 在二维码中间添加Logo图案 + * @param src + * @param logo + * @param ratio logo所占比例 因为二维码的最大容错率为30%,所以建议ratio的范围小于0.3 + * @return + */ + private fun addLogo( + src: Bitmap?, + logo: Bitmap?, + @FloatRange(from = 0.0, to = 1.0) ratio: Float + ): Bitmap? { + if (src == null) { + return null + } + if (logo == null) { + return src + } + + //获取图片的宽高 + val srcWidth = src.width + val srcHeight = src.height + val logoWidth = logo.width + val logoHeight = logo.height + if (srcWidth == 0 || srcHeight == 0) { + return null + } + if (logoWidth == 0 || logoHeight == 0) { + return src + } + + //logo大小为二维码整体大小 + val scaleFactor = srcWidth * ratio / logoWidth + var bitmap: Bitmap? = Bitmap.createBitmap(srcWidth, srcHeight, Bitmap.Config.ARGB_8888) + try { + val canvas = Canvas(bitmap!!) + canvas.drawBitmap(src, 0f, 0f, null) + canvas.scale( + scaleFactor, + scaleFactor, + (srcWidth / 2).toFloat(), + (srcHeight / 2).toFloat() + ) + canvas.drawBitmap( + logo, + ((srcWidth - logoWidth) / 2).toFloat(), + ((srcHeight - logoHeight) / 2).toFloat(), + null + ) + canvas.save() + canvas.restore() + } catch (e: Exception) { + bitmap = null + e.printOnDebug() + } + return bitmap + } + + /** + * 解析一维码/二维码图片 + * @param bitmap 解析的图片 + * @param hints 解析编码类型 + * @return + */ + fun parseCode( + bitmap: Bitmap, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): String? { + val result = parseCodeResult(bitmap, reqWidth, reqHeight, hints) + return result?.text + } + + /** + * 解析一维码/二维码图片 + * @param bitmap 解析的图片 + * @param hints 解析编码类型 + * @return + */ + fun parseCodeResult( + bitmap: Bitmap, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): Result? { + if (bitmap.width > reqWidth || bitmap.height > reqHeight) { + val bm = bitmap.resizeAndRecycle(reqWidth, reqHeight) + return parseCodeResult(getRGBLuminanceSource(bm), hints) + } + return parseCodeResult(getRGBLuminanceSource(bitmap), hints) + } + + /** + * 解析一维码/二维码图片 + * @param source + * @param hints + * @return + */ + fun parseCodeResult(source: LuminanceSource?, hints: Map?): Result? { + var result: Result? = null + val reader = MultiFormatReader() + try { + reader.setHints(hints) + if (source != null) { + result = decodeInternal(reader, source) + if (result == null) { + result = decodeInternal(reader, source.invert()) + } + if (result == null && source.isRotateSupported) { + result = decodeInternal(reader, source.rotateCounterClockwise()) + } + } + } catch (e: java.lang.Exception) { + e.printOnDebug() + } finally { + reader.reset() + } + return result + } + + /** + * 解析二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @return + */ + fun parseQRCode(bitmapPath: String?): String? { + val result = parseQRCodeResult(bitmapPath) + return result?.text + } + + /** + * 解析二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @param reqWidth 请求目标宽度,如果实际图片宽度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @param reqHeight 请求目标高度,如果实际图片高度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @return + */ + fun parseQRCodeResult( + bitmapPath: String?, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT + ): Result? { + return parseCodeResult(bitmapPath, reqWidth, reqHeight, DecodeFormatManager.QR_CODE_HINTS) + } + + /** + * 解析一维码/二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @return + */ + fun parseCode( + bitmapPath: String?, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): String? { + return parseCodeResult(bitmapPath, reqWidth, reqHeight, hints)?.text + } + + /** + * 解析一维码/二维码图片 + * @param bitmapPath 需要解析的图片路径 + * @param reqWidth 请求目标宽度,如果实际图片宽度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @param reqHeight 请求目标高度,如果实际图片高度大于此值,会自动进行压缩处理,当 reqWidth 和 reqHeight都小于或等于0时,则不进行压缩处理 + * @param hints 解析编码类型 + * @return + */ + fun parseCodeResult( + bitmapPath: String?, + reqWidth: Int = DEFAULT_REQ_WIDTH, + reqHeight: Int = DEFAULT_REQ_HEIGHT, + hints: Map = DecodeFormatManager.ALL_HINTS + ): Result? { + var result: Result? = null + val reader = MultiFormatReader() + try { + reader.setHints(hints) + val source = getRGBLuminanceSource(compressBitmap(bitmapPath, reqWidth, reqHeight)) + result = decodeInternal(reader, source) + if (result == null) { + result = decodeInternal(reader, source.invert()) + } + if (result == null && source.isRotateSupported) { + result = decodeInternal(reader, source.rotateCounterClockwise()) + } + } catch (e: Exception) { + e.printOnDebug() + } finally { + reader.reset() + } + return result + } + + private fun decodeInternal(reader: MultiFormatReader, source: LuminanceSource): Result? { + var result: Result? = null + try { + try { + //采用HybridBinarizer解析 + result = reader.decodeWithState(BinaryBitmap(HybridBinarizer(source))) + } catch (_: Exception) { + } + if (result == null) { + //如果没有解析成功,再采用GlobalHistogramBinarizer解析一次 + result = reader.decodeWithState(BinaryBitmap(GlobalHistogramBinarizer(source))) + } + } catch (_: Exception) { + } + return result + } + + + /** + * 压缩图片 + * @param path + * @return + */ + private fun compressBitmap(path: String?, reqWidth: Int, reqHeight: Int): Bitmap { + if (reqWidth > 0 && reqHeight > 0) { //都大于进行判断是否压缩 + val newOpts = BitmapFactory.Options() + // 开始读入图片,此时把options.inJustDecodeBounds 设回true了 + newOpts.inJustDecodeBounds = true //获取原始图片大小 + BitmapFactory.decodeFile(path, newOpts) // 此时返回bm为空 + val width = newOpts.outWidth.toFloat() + val height = newOpts.outHeight.toFloat() + // 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可 + var wSize = 1 // wSize=1表示不缩放 + if (width > reqWidth) { // 如果宽度大的话根据宽度固定大小缩放 + wSize = (width / reqWidth).toInt() + } + var hSize = 1 // wSize=1表示不缩放 + if (height > reqHeight) { // 如果高度高的话根据宽度固定大小缩放 + hSize = (height / reqHeight).toInt() + } + var size = max(wSize, hSize) + if (size <= 0) size = 1 + newOpts.inSampleSize = size // 设置缩放比例 + // 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了 + newOpts.inJustDecodeBounds = false + return BitmapFactory.decodeFile(path, newOpts) + } + return BitmapFactory.decodeFile(path) + } + + + /** + * 获取RGBLuminanceSource + * @param bitmap + * @return + */ + private fun getRGBLuminanceSource(bitmap: Bitmap): RGBLuminanceSource { + val width = bitmap.width + val height = bitmap.height + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) + return RGBLuminanceSource(width, height, pixels) + } + + /** + * 生成条形码 + * @param content + * @param format + * @param desiredWidth + * @param desiredHeight + * @param hints + * @param isShowText + * @param textSize + * @param codeColor + * @return + */ + fun createBarCode( + content: String?, + desiredWidth: Int, + desiredHeight: Int, + format: BarcodeFormat = BarcodeFormat.CODE_128, + hints: Map? = null, + isShowText: Boolean = true, + textSize: Int = 40, + @ColorInt codeColor: Int = Color.BLACK + ): Bitmap? { + if (TextUtils.isEmpty(content)) { + return null + } + val writer = MultiFormatWriter() + try { + val result = writer.encode( + content, format, desiredWidth, + desiredHeight, hints + ) + val width = result.width + val height = result.height + val pixels = IntArray(width * height) + // All are 0, or black, by default + for (y in 0 until height) { + val offset = y * width + for (x in 0 until width) { + pixels[offset + x] = if (result[x, y]) codeColor else Color.WHITE + } + } + val bitmap = Bitmap.createBitmap( + width, height, + Bitmap.Config.ARGB_8888 + ) + bitmap.setPixels(pixels, 0, width, 0, 0, width, height) + return if (isShowText) { + addCode(bitmap, content, textSize, codeColor, textSize / 2) + } else bitmap + } catch (e: WriterException) { + e.printOnDebug() + } + return null + } + + /** + * 条形码下面添加文本信息 + * @param src + * @param code + * @param textSize + * @param textColor + * @return + */ + private fun addCode( + src: Bitmap?, + code: String?, + textSize: Int, + @ColorInt textColor: Int, + offset: Int + ): Bitmap? { + if (src == null) { + return null + } + if (TextUtils.isEmpty(code)) { + return src + } + + //获取图片的宽高 + val srcWidth = src.width + val srcHeight = src.height + if (srcWidth <= 0 || srcHeight <= 0) { + return null + } + var bitmap: Bitmap? = Bitmap.createBitmap( + srcWidth, + srcHeight + textSize + offset * 2, + Bitmap.Config.ARGB_8888 + ) + try { + val canvas = Canvas(bitmap!!) + canvas.drawBitmap(src, 0f, 0f, null) + val paint = TextPaint() + paint.textSize = textSize.toFloat() + paint.color = textColor + paint.textAlign = Paint.Align.CENTER + canvas.drawText( + code!!, + (srcWidth / 2).toFloat(), + (srcHeight + textSize / 2 + offset).toFloat(), + paint + ) + canvas.save() + canvas.restore() + } catch (e: Exception) { + bitmap = null + e.printOnDebug() + } + return bitmap + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/RandomColor.kt b/app/src/main/java/io/legado/app/utils/RandomColor.kt new file mode 100755 index 000000000..526afa6cc --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RandomColor.kt @@ -0,0 +1,61 @@ +package io.legado.app.utils + +import android.graphics.Color +import java.util.* + +@Suppress("unused") +class RandomColor(alpha: Int, lower: Int, upper: Int) { + + constructor() : this(255, 80, 200) + + private var alpha: Int = 0 + private var lower: Int = 0 + private var upper: Int = 0 + + init { + require(upper > lower) { "must be lower < upper" } + setAlpha(alpha) + setLower(lower) + setUpper(upper) + } + + //随机数是前闭 后开 + fun build(): Int { + val red = getLower() + Random().nextInt(getUpper() - getLower() + 1) + val green = getLower() + Random().nextInt(getUpper() - getLower() + 1) + val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1) + return Color.argb(getAlpha(), red, green, blue) + } + + private fun getAlpha(): Int { + return alpha + } + + private fun setAlpha(alpha: Int) { + var alpha1 = alpha + if (alpha1 > 255) alpha1 = 255 + if (alpha1 < 0) alpha1 = 0 + this.alpha = alpha1 + } + + private fun getLower(): Int { + return lower + } + + private fun setLower(lower: Int) { + var lower1 = lower + if (lower1 < 0) lower1 = 0 + this.lower = lower1 + } + + private fun getUpper(): Int { + return upper + } + + private fun setUpper(upper: Int) { + var upper1 = upper + if (upper1 > 255) upper1 = 255 + this.upper = upper1 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/RealPathUtil.kt b/app/src/main/java/io/legado/app/utils/RealPathUtil.kt new file mode 100755 index 000000000..a2f5f317b --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RealPathUtil.kt @@ -0,0 +1,185 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import androidx.core.provider.DocumentsContractCompat + +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException + +@Suppress("unused") +object RealPathUtil { + /** + * Method for return file path of Gallery image + * @return path of the selected image file from gallery + */ + private var filePathUri: Uri? = null + + fun getPath(context: Context, uri: Uri): String? { + //check here to KITKAT or new version + @SuppressLint("ObsoleteSdkInt") + val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + filePathUri = uri + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } + } else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + //return getDataColumn(context, uri, null, null); + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + var contentUri: Uri? = null + when (type) { + "image" -> { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + "video" -> { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + + "audio" -> { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } else if (DocumentsContractCompat.isTreeUri(uri)) { + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getTreeDocumentId(uri) + val split = docId.split(":") + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment + else getDataColumn(context, uri, null, null) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return uri.path + } + + fun getTreePath(uri: Uri): String? { + if (!DocumentsContractCompat.isTreeUri(uri) || !isExternalStorageDocument(uri)) { + return null + } + val docId = DocumentsContract.getTreeDocumentId(uri) + val split = docId.split(":") + if (split.size < 2) { + return null + } + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private fun getDataColumn( + context: Context, uri: Uri?, selection: String?, + selectionArgs: Array? + ): String? { + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf( + column + ) + try { + cursor = + context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + return cursor.getString(index) + } + } catch (e: IllegalArgumentException) { + e.printOnDebug() + val file = File(context.cacheDir, "tmp") + val filePath = file.absolutePath + try { + return context.contentResolver.openFileDescriptor(filePathUri!!, "r")?.use { + val fd = it.fileDescriptor + FileInputStream(fd).use { fis -> + FileOutputStream(filePath).use { fos -> + fis.copyTo(fos) + } + } + File(filePath).absolutePath + } + } catch (e: IOException) { + e.printStackTrace() + } + } finally { + cursor?.close() + } + return null + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt b/app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt new file mode 100755 index 000000000..f8c308a26 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt @@ -0,0 +1,30 @@ +package io.legado.app.utils + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +fun RecyclerView.findCenterViewPosition(): Int { + return getChildAdapterPosition( + findChildViewUnder(width / 2f, height / 2f) ?: return RecyclerView.NO_POSITION + ) +} + +fun RecyclerView.findViewPosition(x: Float, y: Float): Int { + return getChildAdapterPosition(findChildViewUnder(x, y) ?: return RecyclerView.NO_POSITION) +} + +fun RecyclerView.findFirstVisibleViewPosition(): Int { + var pos = -1 + if (layoutManager is LinearLayoutManager) { + pos = (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + } + return pos +} + +fun RecyclerView.findLastVisibleViewPosition(): Int { + var pos = -1 + if (layoutManager is LinearLayoutManager) { + pos = (layoutManager as LinearLayoutManager).findLastVisibleItemPosition() + } + return pos +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/RegexExtensions.kt b/app/src/main/java/io/legado/app/utils/RegexExtensions.kt new file mode 100755 index 000000000..d3ac1778f --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RegexExtensions.kt @@ -0,0 +1,78 @@ +package io.legado.app.utils + +import com.script.ScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.exception.RegexTimeoutException +import io.legado.app.help.CrashHandler +import io.legado.app.help.coroutine.Coroutine +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.selects.onTimeout +import kotlinx.coroutines.selects.select +import kotlinx.coroutines.suspendCancellableCoroutine +import splitties.init.appCtx +import java.util.regex.Matcher +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private val handler by lazy { buildMainHandler() } + +/** + * 带有超时检测的正则替换 + */ +@OptIn(ExperimentalCoroutinesApi::class) +fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String { + val charSequence = this@replace + val isJs = replacement.startsWith("@js:") + val replacement1 = if (isJs) replacement.substring(4) else replacement + return runBlocking { + suspendCancellableCoroutine { block -> + Coroutine.async(executeContext = IO) { + val job = launch { + try { + val pattern = regex.toPattern() + val matcher = pattern.matcher(charSequence) + val stringBuffer = StringBuffer() + while (matcher.find()) { + if (isJs) { + val jsResult = RhinoScriptEngine.run { + val bindings = ScriptBindings() + bindings["result"] = matcher.group() + eval(replacement1, bindings) + }.toString() + val quotedResult = Matcher.quoteReplacement(jsResult) + matcher.appendReplacement(stringBuffer, quotedResult) + } else { + matcher.appendReplacement(stringBuffer, replacement1) + } + } + matcher.appendTail(stringBuffer) + block.resume(stringBuffer.toString()) + } catch (e: Exception) { + block.resumeWithException(e) + } + } + select { + job.onJoin {} + onTimeout(timeout) { + val timeoutMsg = + "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:$charSequence" + val exception = RegexTimeoutException(timeoutMsg) + block.cancel(exception) + appCtx.longToastOnUi(timeoutMsg) + CrashHandler.saveCrashInfo2File(exception) + select { + job.onJoin {} + onTimeout(3000) { + appCtx.restart() + } + } + } + } + } + } + } +} + diff --git a/app/src/main/java/io/legado/app/utils/RequestManagerExtensions.kt b/app/src/main/java/io/legado/app/utils/RequestManagerExtensions.kt new file mode 100755 index 000000000..647fe0524 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RequestManagerExtensions.kt @@ -0,0 +1,30 @@ +package io.legado.app.utils + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestManager +import splitties.init.appCtx + +private val applicationRequestManager by lazy { + Glide.with(appCtx) +} + +fun RequestManager.lifecycle(lifecycle: Lifecycle): RequestManager { + if (this === applicationRequestManager) { + return this + } + + val observer = object : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) = onStart() + override fun onPause(owner: LifecycleOwner) = onStop() + override fun onDestroy(owner: LifecycleOwner) { + owner.lifecycle.removeObserver(this) + } + } + + lifecycle.addObserver(observer) + + return this +} diff --git a/app/src/main/java/io/legado/app/utils/Snackbars.kt b/app/src/main/java/io/legado/app/utils/Snackbars.kt new file mode 100755 index 000000000..1adeeabda --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/Snackbars.kt @@ -0,0 +1,169 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.view.View +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar + +/** + * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. + * + * @param message the message text resource. + */ +@JvmName("snackbar2") +fun View.snackbar( + @StringRes message: Int +) = Snackbar + .make(this, message, Snackbar.LENGTH_SHORT) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. + * + * @param message the message text resource. + */ +@JvmName("longSnackbar2") +fun View.longSnackbar( + @StringRes message: Int +) = Snackbar + .make(this, message, Snackbar.LENGTH_LONG) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. + * + * @param message the message text resource. + */ +@JvmName("indefiniteSnackbar2") +fun View.indefiniteSnackbar( + @StringRes message: Int +) = Snackbar + .make(this, message, Snackbar.LENGTH_INDEFINITE) + .apply { show() } + +/** + * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. + * + * @param message the message text. + */ +@JvmName("snackbar2") +fun View.snackbar( + message: CharSequence +) = Snackbar + .make(this, message, Snackbar.LENGTH_SHORT) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. + * + * @param message the message text. + */ +@JvmName("longSnackbar2") +fun View.longSnackbar( + message: CharSequence +) = Snackbar + .make(this, message, Snackbar.LENGTH_LONG) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. + * + * @param message the message text. + */ +@JvmName("indefiniteSnackbar2") +fun View.indefiniteSnackbar( + message: CharSequence +) = Snackbar + .make(this, message, Snackbar.LENGTH_INDEFINITE) + .apply { show() } + +/** + * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. + * + * @param message the message text resource. + */ +@JvmName("snackbar2") +fun View.snackbar( + message: Int, + @StringRes actionText: + Int, action: (View) -> Unit +) = Snackbar + .make(this, message, Snackbar.LENGTH_SHORT) + .setAction(actionText, action) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. + * + * @param message the message text resource. + */ +@JvmName("longSnackbar2") +fun View.longSnackbar( + @StringRes message: Int, + @StringRes actionText: Int, + action: (View) -> Unit +) = Snackbar + .make(this, message, Snackbar.LENGTH_LONG) + .setAction(actionText, action) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. + * + * @param message the message text resource. + */ +@JvmName("indefiniteSnackbar2") +fun View.indefiniteSnackbar( + @StringRes message: Int, + @StringRes actionText: Int, + action: (View) -> Unit +) = Snackbar + .make(this, message, Snackbar.LENGTH_INDEFINITE) + .setAction(actionText, action) + .apply { show() } + +/** + * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. + * + * @param message the message text. + */ +@JvmName("snackbar2") +fun View.snackbar( + message: CharSequence, + actionText: CharSequence, + action: (View) -> Unit +) = Snackbar + .make(this, message, Snackbar.LENGTH_SHORT) + .setAction(actionText, action) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. + * + * @param message the message text. + */ +@JvmName("longSnackbar2") +fun View.longSnackbar( + message: CharSequence, + actionText: CharSequence, + action: (View) -> Unit +) = Snackbar + .make(this, message, Snackbar.LENGTH_LONG) + .setAction(actionText, action) + .apply { show() } + +/** + * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. + * + * @param message the message text. + */ +@JvmName("indefiniteSnackbar2") +fun View.indefiniteSnackbar( + message: CharSequence, + actionText: CharSequence, + action: (View) -> Unit +) = Snackbar + .make(this, message, Snackbar.LENGTH_INDEFINITE) + .setAction(actionText, action) + .apply { show() } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/StringExtensions.kt b/app/src/main/java/io/legado/app/utils/StringExtensions.kt new file mode 100755 index 000000000..5de0b9086 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/StringExtensions.kt @@ -0,0 +1,146 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.icu.text.Collator +import android.icu.util.ULocale +import android.net.Uri +import android.text.Editable +import cn.hutool.core.net.URLEncodeUtil +import io.legado.app.constant.AppPattern +import io.legado.app.constant.AppPattern.dataUriRegex +import java.io.File +import java.lang.Character.codePointCount +import java.lang.Character.offsetByCodePoints +import java.util.Locale +import java.util.regex.Pattern + +fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() + +fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true + +fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) + +fun String.parseToUri(): Uri { + return if (isUri()) Uri.parse(this) else { + Uri.fromFile(File(this)) + } +} + +fun String?.isUri(): Boolean { + this ?: return false + return this.startsWith("file://", true) || isContentScheme() +} + +fun String?.isAbsUrl() = + this?.let { + it.startsWith("http://", true) || it.startsWith("https://", true) + } ?: false + +fun String?.isDataUrl() = + this?.let { + dataUriRegex.matches(it) + } ?: false + +fun String?.isJson(): Boolean = + this?.run { + val str = this.trim() + when { + str.startsWith("{") && str.endsWith("}") -> true + str.startsWith("[") && str.endsWith("]") -> true + else -> false + } + } ?: false + +fun String?.isJsonObject(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("{") && str.endsWith("}") + } ?: false + +fun String?.isJsonArray(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("[") && str.endsWith("]") + } ?: false + +fun String?.isXml(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("<") && str.endsWith(">") + } ?: false + +fun String?.isTrue(nullIsTrue: Boolean = false): Boolean { + if (this.isNullOrBlank() || this == "null") { + return nullIsTrue + } + return !this.trim().matches("(?i)^(false|no|not|0)$".toRegex()) +} + +fun String.isHex(): Boolean { + return all {c -> + c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f' + } +} + +fun String.splitNotBlank(vararg delimiter: String, limit: Int = 0): Array = run { + this.split(*delimiter, limit = limit).map { it.trim() }.filterNot { it.isBlank() } + .toTypedArray() +} + +fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { + this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() +} + +@SuppressLint("ObsoleteSdkInt") +fun String.cnCompare(other: String): Int { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Collator.getInstance(ULocale.SIMPLIFIED_CHINESE).compare(this, other) + } else { + java.text.Collator.getInstance(Locale.CHINA).compare(this, other) + } +} + +/** + * 字符串所占内存大小 + */ +fun String?.memorySize(): Int { + this ?: return 0 + return 40 + 2 * length +} + +/** + * 是否中文 + */ +fun String.isChinese(): Boolean { + val p = Pattern.compile("[\u4e00-\u9fa5]") + val m = p.matcher(this) + return m.find() +} + +/** + * 将字符串拆分为单个字符,包含emoji + */ +fun CharSequence.toStringArray(): Array { + var codePointIndex = 0 + return try { + Array(codePointCount(this, 0, length)) { + val start = codePointIndex + codePointIndex = offsetByCodePoints(this, start, 1) + substring(start, codePointIndex) + } + } catch (e: Exception) { + split("").toTypedArray() + } +} + +fun String.escapeRegex(): String { + return replace(AppPattern.regexCharRegex, "\\\\$0") +} + +fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this) + +fun String.normalizeFileName(): String { + return replace(AppPattern.fileNameRegex2, "_") +} diff --git a/app/src/main/java/io/legado/app/utils/StringUtils.kt b/app/src/main/java/io/legado/app/utils/StringUtils.kt new file mode 100755 index 000000000..acc118d12 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/StringUtils.kt @@ -0,0 +1,370 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.text.TextUtils.isEmpty +import android.util.Base64 +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale +import java.util.regex.Matcher +import java.util.regex.Pattern +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import kotlin.math.abs + + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object StringUtils { + private const val HOUR_OF_DAY = 24 + private const val DAY_OF_YESTERDAY = 2 + private const val TIME_UNIT = 60 + private val ChnMap = chnMap + private val wordCountFormatter by lazy { + DecimalFormat("#.#") + } + + private val chnMap: HashMap + get() { + val map = HashMap() + var cnStr = "零一二三四五六七八九十" + var c = cnStr.toCharArray() + for (i in 0..10) { + map[c[i]] = i + } + cnStr = "〇壹贰叁肆伍陆柒捌玖拾" + c = cnStr.toCharArray() + for (i in 0..10) { + map[c[i]] = i + } + map['两'] = 2 + map['百'] = 100 + map['佰'] = 100 + map['千'] = 1000 + map['仟'] = 1000 + map['万'] = 10000 + map['亿'] = 100000000 + return map + } + + /** + * 将日期转换成昨天、今天、明天 + */ + fun dateConvert(source: String, pattern: String): String { + val format = SimpleDateFormat(pattern, Locale.getDefault()) + val calendar = Calendar.getInstance() + kotlin.runCatching { + val date = format.parse(source) ?: return "" + val curTime = calendar.timeInMillis + calendar.time = date + //将MISC 转换成 sec + val difSec = abs((curTime - date.time) / 1000) + val difMin = difSec / 60 + val difHour = difMin / 60 + val difDate = difHour / 60 + val oldHour = calendar.get(Calendar.HOUR) + //如果没有时间 + if (oldHour == 0) { + //比日期:昨天今天和明天 + return when { + difDate == 0L -> "今天" + difDate < DAY_OF_YESTERDAY -> "昨天" + else -> { + @SuppressLint("SimpleDateFormat") + val convertFormat = SimpleDateFormat("yyyy-MM-dd") + convertFormat.format(date) + } + } + } + + return when { + difSec < TIME_UNIT -> difSec.toString() + "秒前" + difMin < TIME_UNIT -> difMin.toString() + "分钟前" + difHour < HOUR_OF_DAY -> difHour.toString() + "小时前" + difDate < DAY_OF_YESTERDAY -> "昨天" + else -> { + @SuppressLint("SimpleDateFormat") + val convertFormat = SimpleDateFormat("yyyy-MM-dd") + convertFormat.format(date) + } + } + }.onFailure { + it.printOnDebug() + } + return "" + } + + /** + * 首字母大写 + */ + @SuppressLint("DefaultLocale") + fun toFirstCapital(str: String): String { + return str.substring(0, 1).uppercase(Locale.getDefault()) + str.substring(1) + } + + /** + * 将文本中的半角字符,转换成全角字符 + */ + fun halfToFull(input: String): String { + val c = input.toCharArray() + for (i in c.indices) { + if (c[i].code == 32) + //半角空格 + { + c[i] = 12288.toChar() + continue + } + //根据实际情况,过滤不需要转换的符号 + //if (c[i] == 46) //半角点号,不转换 + // continue; + + if (c[i].code in 33..126) + //其他符号都转换为全角 + c[i] = (c[i].code + 65248).toChar() + } + return String(c) + } + + /** + * 字符串全角转换为半角 + */ + fun fullToHalf(input: String): String { + val c = input.toCharArray() + for (i in c.indices) { + if (c[i].code == 12288) + //全角空格 + { + c[i] = 32.toChar() + continue + } + + if (c[i].code in 65281..65374) + c[i] = (c[i].code - 65248).toChar() + } + return String(c) + } + + /** + * 中文大写数字转数字 + */ + fun chineseNumToInt(chNum: String): Int { + var result = 0 + var tmp = 0 + var billion = 0 + val cn = chNum.toCharArray() + + // "一零二五" 形式 + if (cn.size > 1 && chNum.matches("^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$".toRegex())) { + for (i in cn.indices) { + cn[i] = (48 + ChnMap[cn[i]]!!).toChar() + } + return Integer.parseInt(String(cn)) + } + + // "一千零二十五", "一千二" 形式 + return kotlin.runCatching { + for (i in cn.indices) { + val tmpNum = ChnMap[cn[i]]!! + when { + tmpNum == 100000000 -> { + result += tmp + result *= tmpNum + billion = billion * 100000000 + result + result = 0 + tmp = 0 + } + + tmpNum == 10000 -> { + result += tmp + result *= tmpNum + tmp = 0 + } + + tmpNum >= 10 -> { + if (tmp == 0) + tmp = 1 + result += tmpNum * tmp + tmp = 0 + } + + else -> { + tmp = if (i >= 2 && i == cn.size - 1 && ChnMap[cn[i - 1]]!! > 10) + tmpNum * ChnMap[cn[i - 1]]!! / 10 + else + tmp * 10 + tmpNum + } + } + } + result += tmp + billion + result + }.getOrDefault(-1) + } + + /** + * 字符串转数字 + */ + fun stringToInt(str: String?): Int { + if (str != null) { + val num = fullToHalf(str).replace("\\s+".toRegex(), "") + return kotlin.runCatching { + Integer.parseInt(num) + }.getOrElse { + chineseNumToInt(num) + } + } + return -1 + } + + /** + * 是否包含数字 + */ + fun isContainNumber(company: String): Boolean { + val p = Pattern.compile("[0-9]+") + val m = p.matcher(company) + return m.find() + } + + /** + * 是否数字 + */ + fun isNumeric(str: String): Boolean { + val pattern = Pattern.compile("-?[0-9]+") + val isNum = pattern.matcher(str) + return isNum.matches() + } + + fun wordCountFormat(words: Int): String { + var wordsS = "" + if (words > 0) { + if (words > 10000) { + val df = wordCountFormatter + wordsS = df.format(words * 1.0f / 10000f.toDouble()) + "万字" + } else { + wordsS = words.toString() + "字" + } + } + return wordsS + } + + fun wordCountFormat(wc: String?): String { + if (wc == null) return "" + var wordsS = "" + if (isNumeric(wc)) { + val words: Int = wc.toInt() + if (words > 0) { + if (words > 10000) { + val df = wordCountFormatter + wordsS = df.format(words * 1.0f / 10000f.toDouble()) + "万字" + } else { + wordsS = words.toString() + "字" + } + } + } else { + wordsS = wc + } + return wordsS + } + + /** + * 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格) + */ + fun trim(s: String): String { + if (isEmpty(s)) return "" + var start = 0 + val len = s.length + var end = len - 1 + while (start < end && (s[start].code <= 0x20 || s[start] == ' ')) { + ++start + } + while (start < end && (s[end].code <= 0x20 || s[end] == ' ')) { + --end + } + ++end + return if (start > 0 || end < len) s.substring(start, end) else s + } + + /** + * 重复字符串 + */ + fun repeat(str: String, n: Int): String { + val stringBuilder = StringBuilder() + for (i in 0 until n) { + stringBuilder.append(str) + } + return stringBuilder.toString() + } + + /** + * 移除UTF头 + */ + fun removeUTFCharacters(data: String?): String? { + if (data == null) return null + val p = Pattern.compile("\\\\u(\\p{XDigit}{4})") + val m = p.matcher(data) + val buf = StringBuffer(data.length) + while (m.find()) { + val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString() + m.appendReplacement(buf, Matcher.quoteReplacement(ch)) + } + m.appendTail(buf) + return buf.toString() + } + + /** + * 压缩字符串 + */ + fun compress(str: String): Result { + return kotlin.runCatching { + if (str.isEmpty()) { + return@runCatching str + } + val out = ByteArrayOutputStream() + var gzip: GZIPOutputStream? = null + return@runCatching try { + gzip = GZIPOutputStream(out) + gzip.write(str.toByteArray()) + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP) + } finally { + gzip?.runCatching { + close() + } + out.runCatching { + close() + } + } + } + } + + /** + * 解压字符串 + */ + @Throws(IOException::class) + fun unCompress(str: String): Result { + return kotlin.runCatching { + val outputStream = ByteArrayOutputStream() + var inputStream: ByteArrayInputStream? = null + var ginZip: GZIPInputStream? = null + return@runCatching try { + val compressed = Base64.decode(str, Base64.NO_WRAP) + inputStream = ByteArrayInputStream(compressed) + ginZip = GZIPInputStream(inputStream) + ginZip.copyTo(outputStream) + outputStream.toString() + } finally { + ginZip?.runCatching { + close() + } + inputStream?.runCatching { + close() + } + outputStream.runCatching { + close() + } + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/SvgUtils.kt b/app/src/main/java/io/legado/app/utils/SvgUtils.kt new file mode 100755 index 000000000..d1906e46e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/SvgUtils.kt @@ -0,0 +1,85 @@ +package io.legado.app.utils + +import android.graphics.Canvas +import android.graphics.Bitmap +import android.graphics.RectF +import android.util.Size +import java.io.FileInputStream +import java.io.InputStream +import com.caverock.androidsvg.SVG +import kotlin.math.max + +@Suppress("WeakerAccess", "MemberVisibilityCanBePrivate") +object SvgUtils { + + /** + * 从Svg中解码bitmap + */ + + fun createBitmap(filePath: String, width: Int, height: Int? = null): Bitmap? { + return kotlin.runCatching { + val inputStream = FileInputStream(filePath) + createBitmap(inputStream, width, height) + }.getOrNull() + } + + fun createBitmap(inputStream: InputStream, width: Int, height: Int? = null): Bitmap? { + return kotlin.runCatching { + val svg = SVG.getFromInputStream(inputStream) + createBitmap(svg, width, height) + }.getOrNull() + } + + //获取svg图片大小 + fun getSize(filePath: String): Size? { + return kotlin.runCatching { + val inputStream = FileInputStream(filePath) + getSize(inputStream) + }.getOrNull() + } + + fun getSize(inputStream: InputStream): Size? { + return kotlin.runCatching { + val svg = SVG.getFromInputStream(inputStream) + getSize(svg) + }.getOrNull() + } + + /////// private method + private fun createBitmap(svg: SVG, width: Int? = null, height: Int? = null): Bitmap { + val size = getSize(svg) + val wRatio = width?.let { size.width / it } ?: -1 + val hRatio = height?.let { size.height / it } ?: -1 + //如果超出指定大小,则缩小相应的比例 + val ratio = when { + wRatio > 1 && hRatio > 1 -> max(wRatio, hRatio) + wRatio > 1 -> wRatio + hRatio > 1 -> hRatio + else -> 1 + } + + val viewBox: RectF? = svg.documentViewBox + if (viewBox == null && size.width > 0 && size.height > 0) { + svg.setDocumentViewBox(0f, 0f, svg.documentWidth, svg.documentHeight) + } + + svg.setDocumentWidth("100%") + svg.setDocumentHeight("100%") + + val bitmapWidth = size.width / ratio + val bitmapHeight = size.height / ratio + val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) + + svg.renderToCanvas(Canvas(bitmap)) + return bitmap + } + + private fun getSize(svg: SVG): Size { + val width = svg.documentWidth.toInt().takeIf { it > 0 } + ?: (svg.documentViewBox.right - svg.documentViewBox.left).toInt() + val height = svg.documentHeight.toInt().takeIf { it > 0 } + ?: (svg.documentViewBox.bottom - svg.documentViewBox.top).toInt() + return Size(width, height) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/SyncedRenderer.kt b/app/src/main/java/io/legado/app/utils/SyncedRenderer.kt new file mode 100755 index 000000000..8ec825221 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/SyncedRenderer.kt @@ -0,0 +1,25 @@ +package io.legado.app.utils + +import android.view.Choreographer + +class SyncedRenderer(val doFrame: (frameTime: Double) -> Unit) { + + private var callback: (Long) -> Unit = {} + + fun start() { + var currTime = System.nanoTime() / 1000000.0 + callback = { + val currTimeMs = it / 1000000.0 + val frameTime = currTimeMs - currTime + currTime = currTimeMs + doFrame(frameTime) + Choreographer.getInstance().postFrameCallback(callback) + } + Choreographer.getInstance().postFrameCallback(callback) + } + + fun stop() { + Choreographer.getInstance().removeFrameCallback(callback) + callback = {} + } +} diff --git a/app/src/main/java/io/legado/app/utils/SystemUtils.kt b/app/src/main/java/io/legado/app/utils/SystemUtils.kt new file mode 100755 index 000000000..f397eb375 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/SystemUtils.kt @@ -0,0 +1,54 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.view.Display +import splitties.init.appCtx +import splitties.systemservices.displayManager +import splitties.systemservices.powerManager + + +@Suppress("unused") +object SystemUtils { + + @SuppressLint("ObsoleteSdkInt") + fun ignoreBatteryOptimization(activity: Activity) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return + + val hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.packageName) + // 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。 + if (!hasIgnored) { + try { + @SuppressLint("BatteryLife") + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + intent.data = Uri.parse("package:" + activity.packageName) + activity.startActivity(intent) + } catch (ignored: Throwable) { + } + + } + } + + fun isScreenOn(): Boolean { + return displayManager.displays.filterNotNull().any { + it.state != Display.STATE_OFF + } + } + + /** + * 屏幕像素宽度 + */ + val screenWidthPx by lazy { + appCtx.resources.displayMetrics.widthPixels + } + + /** + * 屏幕像素高度 + */ + val screenHeightPx by lazy { + appCtx.resources.displayMetrics.heightPixels + } +} diff --git a/app/src/main/java/io/legado/app/utils/Throttle.kt b/app/src/main/java/io/legado/app/utils/Throttle.kt new file mode 100755 index 000000000..bd8cc8266 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/Throttle.kt @@ -0,0 +1,16 @@ +package io.legado.app.utils + +@Suppress("unused") +class Throttle( + wait: Long = 0L, + leading: Boolean = true, + trailing: Boolean = true, + func: () -> T +) : Debounce(wait, wait, leading, trailing, func) + +fun throttle( + wait: Long = 0L, + leading: Boolean = true, + trailing: Boolean = true, + func: () -> T +) = Throttle(wait, leading, trailing, func) diff --git a/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt b/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt new file mode 100755 index 000000000..4163b8211 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ThrowableExtensions.kt @@ -0,0 +1,19 @@ +package io.legado.app.utils + +import java.io.IOException + +val Throwable.stackTraceStr: String + get() { + val stackTrace = stackTraceToString() + val lMsg = this.localizedMessage ?: "noErrorMsg" + return when { + stackTrace.isNotEmpty() -> stackTrace + else -> lMsg + } + } + +fun Throwable.asIOException(): IOException { + val newException = IOException(this.message) + newException.initCause(this) + return newException +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/TimeUtils.kt b/app/src/main/java/io/legado/app/utils/TimeUtils.kt new file mode 100755 index 000000000..93dadb710 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/TimeUtils.kt @@ -0,0 +1,52 @@ +package io.legado.app.utils + +import kotlin.math.abs + +fun Long.toTimeAgo(): String { + val curTime = System.currentTimeMillis() + val time = this + val seconds = abs(System.currentTimeMillis() - time) / 1000f + val end = if (time < curTime) "前" else "后" + + val start = when { + seconds < 60 -> "${seconds.toInt()}秒" + seconds < 3600 -> { + val minutes = seconds / 60f + "${minutes.toInt()}分钟" + } + seconds < 86400 -> { + val hours = seconds / 3600f + "${hours.toInt()}小时" + } + seconds < 604800 -> { + val days = seconds / 86400f + "${days.toInt()}天" + } + seconds < 2_628_000 -> { + val weeks = seconds / 604800f + "${weeks.toInt()}周" + } + seconds < 31_536_000 -> { + val months = seconds / 2_628_000f + "${months.toInt()}月" + } + else -> { + val years = seconds / 31_536_000f + "${years.toInt()}年" + } + } + return start + end +} + +fun Int.toDurationTime(): String { + val totalSeconds = this / 1000 + val hours = totalSeconds / 3600 + val minutes = (totalSeconds % 3600) / 60 + val seconds = totalSeconds % 60 + + return if (hours > 0) { + "%d:%02d:%02d".format(hours, minutes, seconds) + } else { + "%02d:%02d".format(minutes, seconds) + } +} diff --git a/app/src/main/java/io/legado/app/utils/ToastUtils.kt b/app/src/main/java/io/legado/app/utils/ToastUtils.kt new file mode 100755 index 000000000..4fd25c064 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ToastUtils.kt @@ -0,0 +1,86 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.Toast +import androidx.fragment.app.Fragment +import io.legado.app.BuildConfig +import io.legado.app.databinding.ViewToastBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import splitties.systemservices.layoutInflater + +private var toast: Toast? = null + +private var toastLegacy: Toast? = null + +fun Context.toastOnUi(message: Int, duration: Int = Toast.LENGTH_SHORT) { + toastOnUi(getString(message), duration) +} + +@SuppressLint("InflateParams") +@Suppress("DEPRECATION") +fun Context.toastOnUi(message: CharSequence?, duration: Int = Toast.LENGTH_SHORT) { + runOnUI { + kotlin.runCatching { + toast?.cancel() + toast = Toast(this) + val isLight = ColorUtils.isColorLight(bottomBackground) + ViewToastBinding.inflate(layoutInflater).run { + toast?.view = root + cvToast.setCardBackgroundColor(bottomBackground) + tvText.setTextColor(getPrimaryTextColor(isLight)) + tvText.text = message + } + toast?.duration = duration + toast?.show() + } + } +} + +fun Context.toastOnUiLegacy(message: CharSequence) { + runOnUI { + kotlin.runCatching { + if (toastLegacy == null || BuildConfig.DEBUG || AppConfig.recordLog) { + toastLegacy = Toast.makeText(this, message, Toast.LENGTH_SHORT) + } else { + toastLegacy?.setText(message) + toastLegacy?.duration = Toast.LENGTH_SHORT + } + toastLegacy?.show() + } + } +} + +fun Context.longToastOnUi(message: Int) { + toastOnUi(message, Toast.LENGTH_LONG) +} + +fun Context.longToastOnUi(message: CharSequence?) { + toastOnUi(message, Toast.LENGTH_LONG) +} + +fun Context.longToastOnUiLegacy(message: CharSequence) { + runOnUI { + kotlin.runCatching { + if (toastLegacy == null || BuildConfig.DEBUG || AppConfig.recordLog) { + toastLegacy = Toast.makeText(this, message, Toast.LENGTH_LONG) + } else { + toastLegacy?.setText(message) + toastLegacy?.duration = Toast.LENGTH_LONG + } + toastLegacy?.show() + } + } +} + +fun Fragment.toastOnUi(message: Int) = requireActivity().toastOnUi(message) + +fun Fragment.toastOnUi(message: CharSequence) = requireActivity().toastOnUi(message) + +fun Fragment.longToast(message: Int) = requireContext().longToastOnUi(message) + +fun Fragment.longToast(message: CharSequence) = requireContext().longToastOnUi(message) diff --git a/app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt b/app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt new file mode 100755 index 000000000..7f78f73a1 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ToolBarExtensions.kt @@ -0,0 +1,23 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.os.Build +import android.widget.Toolbar +import androidx.core.content.ContextCompat +import io.legado.app.R + +/** + * 设置toolBar更多图标颜色 + */ +@SuppressLint("ObsoleteSdkInt") +fun Toolbar.setMoreIconColor(color: Int) { + val moreIcon = ContextCompat.getDrawable(context, R.drawable.ic_more) + if (moreIcon != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + moreIcon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + overflowIcon = moreIcon + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/UriExtensions.kt b/app/src/main/java/io/legado/app/utils/UriExtensions.kt new file mode 100755 index 000000000..c4bb8d5ab --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/UriExtensions.kt @@ -0,0 +1,316 @@ +package io.legado.app.utils + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.ParcelFileDescriptor +import androidx.appcompat.app.AppCompatActivity +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.exception.NoStackTraceException +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source +import splitties.init.appCtx +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.charset.Charset + +fun Uri.isContentScheme() = this.scheme == "content" + +fun Uri.isFileScheme() = this.scheme == "file" + +/** + * 读取URI + */ +fun AppCompatActivity.readUri( + uri: Uri?, + success: (fileDoc: FileDoc, inputStream: InputStream) -> Unit +) { + uri ?: return + try { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(this, uri) + doc ?: throw NoStackTraceException("未获取到文件") + val fileDoc = FileDoc.fromDocumentFile(doc) + contentResolver.openInputStream(uri)!!.use { inputStream -> + success.invoke(fileDoc, inputStream) + } + } else { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.get_storage_per) + .onGranted { + RealPathUtil.getPath(this, uri)?.let { path -> + val file = File(path) + val fileDoc = FileDoc.fromFile(file) + FileInputStream(file).use { inputStream -> + success.invoke(fileDoc, inputStream) + } + } + } + .request() + } + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取Uri出错\n$uri\n$e", e, true) + if (e is SecurityException) { + throw e + } + } +} + +/** + * 读取URI + */ +fun Fragment.readUri(uri: Uri?, success: (fileDoc: FileDoc, inputStream: InputStream) -> Unit) { + uri ?: return + try { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(requireContext(), uri) + doc ?: throw NoStackTraceException("未获取到文件") + val fileDoc = FileDoc.fromDocumentFile(doc) + requireContext().contentResolver.openInputStream(uri)!!.use { inputStream -> + success.invoke(fileDoc, inputStream) + } + } else { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.get_storage_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + val file = File(path) + val fileDoc = FileDoc.fromFile(file) + FileInputStream(file).use { inputStream -> + success.invoke(fileDoc, inputStream) + } + } + } + .request() + } + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取Uri出错\n$uri\n$e", e, true) + } +} + +@Throws(Exception::class) +fun Uri.readBytes(context: Context): ByteArray { + return if (this.isContentScheme()) { + context.contentResolver.openInputStream(this)?.let { + val len: Int = it.available() + val buffer = ByteArray(len) + it.read(buffer) + it.close() + return buffer + } ?: throw NoStackTraceException("打开文件失败\n${this}") + } else { + val path = RealPathUtil.getPath(context, this) + if (path?.isNotEmpty() == true) { + File(path).readBytes() + } else { + throw NoStackTraceException("获取文件真实地址失败\n${this.path}") + } + } +} + +@Throws(Exception::class) +fun Uri.readText(context: Context): String { + readBytes(context).let { + return String(it) + } +} + +@Throws(Exception::class) +fun Uri.writeBytes( + context: Context, + byteArray: ByteArray +): Boolean { + if (this.isContentScheme()) { + context.contentResolver.openOutputStream(this)?.let { + it.write(byteArray) + it.close() + return true + } + return false + } else { + val path = RealPathUtil.getPath(context, this) + if (path?.isNotEmpty() == true) { + File(path).writeBytes(byteArray) + return true + } + } + return false +} + +@Throws(Exception::class) +fun Uri.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8): Boolean { + return writeBytes(context, text.toByteArray(charset)) +} + +fun Uri.writeBytes( + context: Context, + fileName: String, + byteArray: ByteArray +): Boolean { + if (this.isContentScheme()) { + DocumentFile.fromTreeUri(context, this)?.let { pDoc -> + DocumentUtils.createFileIfNotExist(pDoc, fileName)?.let { + return it.uri.writeBytes(context, byteArray) + } + } + } else { + FileUtils.createFileWithReplace(path + File.separatorChar + fileName) + .writeBytes(byteArray) + return true + } + return false +} + +fun Uri.inputStream(context: Context): Result { + val uri = this + return kotlin.runCatching { + try { + if (isContentScheme()) { + DocumentFile.fromSingleUri(context, uri) + ?: throw NoStackTraceException("未获取到文件") + return@runCatching context.contentResolver.openInputStream(uri)!! + } else { + val path = RealPathUtil.getPath(context, uri) + ?: throw NoStackTraceException("未获取到文件") + val file = File(path) + if (file.exists()) { + return@runCatching FileInputStream(file) + } else { + throw NoStackTraceException("文件不存在") + } + } + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取inputStream失败:${e.localizedMessage}", e) + throw e + } + } +} + +fun Uri.outputStream(context: Context): Result { + val uri = this + return kotlin.runCatching { + try { + if (isContentScheme()) { + DocumentFile.fromSingleUri(context, uri) + ?: throw NoStackTraceException("未获取到文件") + return@runCatching context.contentResolver.openOutputStream(uri)!! + } else { + val path = RealPathUtil.getPath(context, uri) + ?: throw NoStackTraceException("未获取到文件") + val file = File(path) + if (file.exists()) { + return@runCatching FileOutputStream(file) + } else { + throw NoStackTraceException("文件不存在") + } + } + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取inputStream失败:${e.localizedMessage}", e) + throw e + } + } +} + +fun Uri.toReadPfd(context: Context): Result { + val uri = this + return kotlin.runCatching { + try { + if (isContentScheme()) { + DocumentFile.fromSingleUri(context, uri) + ?: throw NoStackTraceException("未获取到文件") + return@runCatching context.contentResolver.openFileDescriptor(uri, "r")!! + } else { + val path = RealPathUtil.getPath(context, uri) + ?: throw NoStackTraceException("未获取到文件") + val file = File(path) + if (file.exists()) { + return@runCatching ParcelFileDescriptor.open( + file, + ParcelFileDescriptor.MODE_READ_ONLY + ) + } else { + throw NoStackTraceException("文件不存在") + } + } + + + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取inputStream失败:${e.localizedMessage}", e) + throw e + } + } +} + +fun Uri.toWritePfd(context: Context): Result { + val uri = this + return kotlin.runCatching { + try { + if (isContentScheme()) { + DocumentFile.fromSingleUri(context, uri) + ?: throw NoStackTraceException("未获取到文件") + return@runCatching context.contentResolver.openFileDescriptor(uri, "w")!! + } else { + val path = RealPathUtil.getPath(context, uri) + ?: throw NoStackTraceException("未获取到文件") + val file = File(path) + if (file.exists()) { + return@runCatching ParcelFileDescriptor.open( + file, + ParcelFileDescriptor.MODE_WRITE_ONLY + ) + } else { + throw NoStackTraceException("文件不存在") + } + } + + + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取inputStream失败:${e.localizedMessage}", e) + throw e + } + } +} + +fun Uri.toRequestBody(contentType: MediaType? = null): RequestBody { + val uri = this + return object : RequestBody() { + override fun contentType() = contentType + + override fun contentLength(): Long { + val length = uri.inputStream(appCtx).getOrThrow().available().toLong() + return if (length > 0) length else -1 + } + + override fun writeTo(sink: BufferedSink) { + uri.inputStream(appCtx).getOrThrow().source().use { source -> + sink.writeAll(source) + } + } + } +} + +fun Uri.canRead(): Boolean { + return appCtx.checkSelfUriPermission( + this, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) == PackageManager.PERMISSION_GRANTED +} diff --git a/app/src/main/java/io/legado/app/utils/UrlUtil.kt b/app/src/main/java/io/legado/app/utils/UrlUtil.kt new file mode 100755 index 000000000..eb5425cb2 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/UrlUtil.kt @@ -0,0 +1,171 @@ +package io.legado.app.utils + +import io.legado.app.BuildConfig +import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern.semicolonRegex +import io.legado.app.help.config.AppConfig +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.CustomUrl +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLDecoder + +object UrlUtil { + + // 有时候文件名在query里,截取path会截到其他内容 + // https://www.example.com/download.php?filename=文件.txt + // https://www.example.com/txt/文件.txt?token=123456 + private val unExpectFileSuffixs = arrayOf( + "php", "html" + ) + + fun replaceReservedChar(text: String): String { + return text.replace("%", "%25") + .replace(" ", "%20") + .replace("\"", "%22") + .replace("#", "%23") + .replace("&", "%26") + .replace("(", "%28") + .replace(")", "%29") + .replace("+", "%2B") + .replace(",", "%2C") + .replace("/", "%2F") + .replace(":", "%3A") + .replace(";", "%3B") + .replace("<", "%3C") + .replace("=", "%3D") + .replace(">", "%3E") + .replace("?", "%3F") + .replace("@", "%40") + .replace("\\", "%5C") + .replace("|", "%7C") + } + + + /* 阅读定义的url,{urlOption} */ + fun getFileName(analyzeUrl: AnalyzeUrl): String? { + return getFileName(analyzeUrl.url, analyzeUrl.headerMap) + } + + /** + * 根据网络url获取文件信息 文件名 + */ + @Suppress("MemberVisibilityCanBePrivate") + fun getFileName(fileUrl: String, headerMap: Map? = null): String? { + return kotlin.runCatching { + val url = URL(fileUrl) + var fileName: String? = getFileNameFromPath(url) + if (fileName == null) { + fileName = getFileNameFromResponseHeader(url, headerMap) + } + fileName + }.getOrNull() + } + + @Suppress("MemberVisibilityCanBePrivate") + private fun getFileNameFromResponseHeader( + url: URL, + headerMap: Map? = null + ): String? { + // HEAD方式获取链接响应头信息 + val conn: HttpURLConnection = url.openConnection() as HttpURLConnection + conn.requestMethod = "HEAD" + // 下载链接可能还需要header才能成功访问 + headerMap?.forEach { (key, value) -> + conn.setRequestProperty(key, value) + } + // 禁止重定向 否则获取不到响应头返回的Location + conn.instanceFollowRedirects = false + conn.connect() + + if (AppConfig.recordLog || BuildConfig.DEBUG) { + val headers = conn.headerFields + val headersString = buildString { + headers.forEach { (key, value) -> + value.forEach { + append(key) + append(": ") + append(it) + append("\n") + } + } + } + AppLog.put("$url response header:\n$headersString") + } + + // val fileSize = conn.getContentLengthLong() / 1024 + /** Content-Disposition 存在三种情况 文件名应该用引号 有些用空格 + * filename="filename" + * filename*="charset''filename" + */ + val raw: String? = conn.getHeaderField("Content-Disposition") + // Location跳转到实际链接 + val redirectUrl: String? = conn.getHeaderField("Location") + + return if (raw != null) { + val fileNames = raw.split(semicolonRegex).filter { it.contains("filename") } + val names = hashSetOf() + fileNames.forEach { + val fileName = it.substringAfter("=") + .trim() + .replace("^\"".toRegex(), "") + .replace("\"$".toRegex(), "") + if (it.contains("filename*")) { + val data = fileName.split("''") + names.add(URLDecoder.decode(data[1], data[0])) + } else { + names.add(fileName) + /* 好像不用这样 + names.add( + String( + fileName.toByteArray(StandardCharsets.ISO_8859_1), + StandardCharsets.UTF_8 + ) + ) + */ + } + } + names.firstOrNull() + } else if (redirectUrl != null) { + val newUrl= URL(URLDecoder.decode(redirectUrl, "UTF-8")) + getFileNameFromPath(newUrl) + } else { + AppLog.put("Cannot obtain URL file name, enable recordLog for response header") + null + } + } + + private fun getFileNameFromPath(fileUrl: URL): String? { + val path = fileUrl.path ?: return null + val suffix = getSuffix(path, "") + return if ( + suffix != "" && !unExpectFileSuffixs.contains(suffix) + ) { + path.substringAfterLast("/") + } else { + AppLog.put("getFileNameFromPath: Unexpected file suffix: $suffix") + null + } + } + + private val fileSuffixRegex = Regex("^[a-z\\d]+$", RegexOption.IGNORE_CASE) + + /* 获取合法的文件后缀 */ + fun getSuffix(str: String, default: String? = null): String { + val suffix = CustomUrl(str).getUrl() + .substringAfterLast("/") + .substringBefore("?") + .substringBefore("#") + .substringAfterLast(".", "") + //检查截取的后缀字符是否合法 [a-zA-Z0-9] + return if (suffix.length > 5 || !suffix.matches(fileSuffixRegex)) { + if (default == null) { + AppLog.put("Cannot find legal suffix:\n target: $str\n suffix: $suffix") + } + default ?: "ext" + } else { + suffix + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/Utf8BomUtils.kt b/app/src/main/java/io/legado/app/utils/Utf8BomUtils.kt new file mode 100755 index 000000000..1de5e9421 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/Utf8BomUtils.kt @@ -0,0 +1,38 @@ +package io.legado.app.utils + +@Suppress("unused") +object Utf8BomUtils { + private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) + + fun removeUTF8BOM(xmlText: String): String { + val bytes = xmlText.toByteArray() + val containsBOM = (bytes.size > 3 + && bytes[0] == UTF8_BOM_BYTES[0] + && bytes[1] == UTF8_BOM_BYTES[1] + && bytes[2] == UTF8_BOM_BYTES[2]) + if (containsBOM) { + return String(bytes, 3, bytes.size - 3) + } + return xmlText + } + + fun removeUTF8BOM(bytes: ByteArray): ByteArray { + val containsBOM = (bytes.size > 3 + && bytes[0] == UTF8_BOM_BYTES[0] + && bytes[1] == UTF8_BOM_BYTES[1] + && bytes[2] == UTF8_BOM_BYTES[2]) + if (containsBOM) { + val copy = ByteArray(bytes.size - 3) + System.arraycopy(bytes, 3, copy, 0, bytes.size - 3) + return copy + } + return bytes + } + + fun hasBom(bytes: ByteArray): Boolean { + return (bytes.size > 3 + && bytes[0] == UTF8_BOM_BYTES[0] + && bytes[1] == UTF8_BOM_BYTES[1] + && bytes[2] == UTF8_BOM_BYTES[2]) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt new file mode 100755 index 000000000..603dc567b --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt @@ -0,0 +1,321 @@ +@file:Suppress("unused") + +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Picture +import android.os.Build +import android.text.Html +import android.view.MotionEvent +import android.view.View +import android.view.View.GONE +import android.view.View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.EdgeEffect +import android.widget.EditText +import android.widget.RadioGroup +import android.widget.SeekBar +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.menu.MenuPopupHelper +import androidx.appcompat.widget.PopupMenu +import androidx.core.graphics.record +import androidx.core.graphics.withTranslation +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.get +import androidx.core.view.marginBottom +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager.widget.ViewPager +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.TintHelper +import io.legado.app.utils.canvasrecorder.CanvasRecorder +import io.legado.app.utils.canvasrecorder.record +import splitties.systemservices.inputMethodManager +import splitties.views.bottomPadding +import splitties.views.topPadding +import java.lang.reflect.Field + + +private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? { + return when (context) { + is AppCompatActivity -> context + is androidx.appcompat.view.ContextThemeWrapper -> getCompatActivity(context.baseContext) + is android.view.ContextThemeWrapper -> getCompatActivity(context.baseContext) + else -> null + } +} + +val View.activity: AppCompatActivity? + get() = getCompatActivity(context) + +fun View.hideSoftInput() = run { + inputMethodManager.hideSoftInputFromWindow(this.windowToken, 0) +} + +fun EditText.showSoftInput() = run { + requestFocus() + inputMethodManager.showSoftInput(this, InputMethodManager.RESULT_SHOWN) +} + +fun View.disableAutoFill() = run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS + } +} + +fun View.applyTint( + @ColorInt color: Int, + isDark: Boolean = AppConfig.isNightTheme +) { + TintHelper.setTintAuto(this, color, false, isDark) +} + +fun View.applyBackgroundTint( + @ColorInt color: Int, + isDark: Boolean = AppConfig.isNightTheme +) { + if (background == null) { + setBackgroundColor(color) + } else { + TintHelper.setTintAuto(this, color, true, isDark) + } +} + +fun RecyclerView.setEdgeEffectColor(@ColorInt color: Int) { + edgeEffectFactory = object : RecyclerView.EdgeEffectFactory() { + override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { + val edgeEffect = super.createEdgeEffect(view, direction) + edgeEffect.color = color + return edgeEffect + } + } +} + +fun ViewPager.setEdgeEffectColor(@ColorInt color: Int) { + try { + val clazz = ViewPager::class.java + for (name in arrayOf("mLeftEdge", "mRightEdge")) { + val field = clazz.getDeclaredField(name) + field.isAccessible = true + val edge = field.get(this) + (edge as EdgeEffect).color = color + } + } catch (ignored: Exception) { + } +} + +fun EditText.disableEdit() { + keyListener = null +} + +fun View.gone() { + if (visibility != GONE) { + visibility = GONE + } +} + +fun View.gone(gone: Boolean) { + if (gone) { + gone() + } else { + visibility = VISIBLE + } +} + +fun View.invisible() { + if (visibility != INVISIBLE) { + visibility = INVISIBLE + } +} + +fun View.visible() { + if (visibility != VISIBLE) { + visibility = VISIBLE + } +} + +fun View.visible(visible: Boolean) { + if (visible && visibility != VISIBLE) { + visibility = VISIBLE + } else if (!visible && visibility == VISIBLE) { + visibility = INVISIBLE + } +} + +fun View.screenshot(bitmap: Bitmap? = null, canvas: Canvas? = null): Bitmap? { + return if (width > 0 && height > 0) { + val screenshot = if (bitmap != null && bitmap.width == width && bitmap.height == height) { + bitmap.eraseColor(Color.TRANSPARENT) + bitmap + } else { + bitmap?.recycle() + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + } + val c = canvas ?: Canvas() + c.setBitmap(screenshot) + c.save() + c.translate(-scrollX.toFloat(), -scrollY.toFloat()) + this.draw(c) + c.restore() + c.setBitmap(null) + screenshot.prepareToDraw() + screenshot + } else { + null + } +} + +fun View.screenshot(picture: Picture) { + if (width > 0 && height > 0) { + picture.record(width, height) { + withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) { + draw(this) + } + } + } +} + +fun View.screenshot(canvasRecorder: CanvasRecorder) { + if (width > 0 && height > 0) { + canvasRecorder.record(width, height) { + draw(this) + } + } +} + +fun View.setPaddingBottom(bottom: Int) { + setPadding(paddingLeft, paddingTop, paddingRight, bottom) +} + +fun SeekBar.progressAdd(int: Int) { + progress += int +} + +fun RadioGroup.getIndexById(id: Int): Int { + for (i in 0 until this.childCount) { + if (id == get(i).id) { + return i + } + } + return 0 +} + +fun RadioGroup.getCheckedIndex(): Int { + for (i in 0 until this.childCount) { + if (checkedRadioButtonId == get(i).id) { + return i + } + } + return 0 +} + +fun RadioGroup.checkByIndex(index: Int) { + check(get(index).id) +} + +@SuppressLint("ObsoleteSdkInt") +fun TextView.setHtml(html: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT) + } else { + @Suppress("DEPRECATION") + text = Html.fromHtml(html) + } +} + +fun TextView.setTextIfNotEqual(charSequence: CharSequence?) { + if (text != charSequence) { + text = charSequence + } +} + +@SuppressLint("RestrictedApi") +fun PopupMenu.show(x: Int, y: Int) { + kotlin.runCatching { + val field: Field = this.javaClass.getDeclaredField("mPopup") + field.isAccessible = true + (field.get(this) as MenuPopupHelper).show(x, y) + }.onFailure { + it.printOnDebug() + } +} + +fun View.shouldHideSoftInput(event: MotionEvent): Boolean { + if (this is EditText) { + val l = intArrayOf(0, 0) + getLocationInWindow(l) + val left = l[0] + val top = l[1] + val bottom = top + getHeight() + val right = left + getWidth() + return !(event.x > left && event.x < right && event.y > top && event.y < bottom) + } + return false +} + +fun View.applyStatusBarPadding(withInitialPadding: Boolean = false) { + val initialPadding = if (withInitialPadding) topPadding else 0 + setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()) + topPadding = initialPadding + insets.top + windowInsets + } +} + +fun View.applyNavigationBarPadding(withInitialPadding: Boolean = false) { + val initialPadding = if (withInitialPadding) bottomPadding else 0 + setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + bottomPadding = initialPadding + windowInsets.navigationBarHeight + windowInsets + } +} + +fun View.applyNavigationBarMargin(withInitialMargin: Boolean = false) { + val initialMargin = if (withInitialMargin) marginBottom else 0 + setOnApplyWindowInsetsListenerCompat { _, windowInsets -> + updateLayoutParams { + bottomMargin = initialMargin + windowInsets.navigationBarHeight + } + windowInsets + } +} + +fun View.setBackgroundKeepPadding(@DrawableRes backgroundResId: Int) { + val paddingLeft = paddingLeft + val paddingTop = paddingTop + val paddingRight = paddingRight + val paddingBottom = paddingBottom + setBackgroundResource(backgroundResId) + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom) +} + +fun View.canScroll(direction: Int): Boolean { + return canScrollVertically(direction) || canScrollHorizontally(direction) +} + +private val requestLayoutBroken = Build.VERSION.SDK_INT <= Build.VERSION_CODES.M + || Build.VERSION.SDK_INT in Build.VERSION_CODES.O..Build.VERSION_CODES.Q + +fun View.setOnApplyWindowInsetsListenerCompat(listener: (View, WindowInsetsCompat) -> WindowInsetsCompat) { + ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> + val windowInsets = listener(view, insets) + if (requestLayoutBroken && isLayoutRequested) { + post { + requestLayout() + } + } + windowInsets + } +} + diff --git a/app/src/main/java/io/legado/app/utils/WebSettingsExtensions.kt b/app/src/main/java/io/legado/app/utils/WebSettingsExtensions.kt new file mode 100755 index 000000000..7c0623f91 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/WebSettingsExtensions.kt @@ -0,0 +1,39 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.os.Build +import android.webkit.WebSettings +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature +import io.legado.app.help.config.AppConfig + +/** + * 设置是否夜间模式 + */ +@SuppressLint("RequiresFeature") +fun WebSettings.setDarkeningAllowed(allow: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + kotlin.runCatching { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(this, allow) + return + }.onFailure { + it.printOnDebug() + } + } + if (AppConfig.isNightTheme) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + @Suppress("DEPRECATION") + WebSettingsCompat.setForceDarkStrategy( + this, + WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING + ) + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + @Suppress("DEPRECATION") + WebSettingsCompat.setForceDark( + this, + WebSettingsCompat.FORCE_DARK_ON + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/WindowInsetsExtensions.kt b/app/src/main/java/io/legado/app/utils/WindowInsetsExtensions.kt new file mode 100755 index 000000000..789610d47 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/WindowInsetsExtensions.kt @@ -0,0 +1,9 @@ +package io.legado.app.utils + +import androidx.core.view.WindowInsetsCompat + +val WindowInsetsCompat.navigationBarHeight + get() = (getInsets(WindowInsetsCompat.Type.systemBars()).bottom - imeHeight).coerceAtLeast(0) + +val WindowInsetsCompat.imeHeight + get() = getInsets(WindowInsetsCompat.Type.ime()).bottom diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt new file mode 100755 index 000000000..ee521b0b4 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt @@ -0,0 +1,36 @@ +package io.legado.app.utils.canvasrecorder + +import androidx.annotation.CallSuper + +abstract class BaseCanvasRecorder : CanvasRecorder { + + @JvmField + protected var isDirty = true + + override fun invalidate() { + isDirty = true + } + + @CallSuper + override fun recycle() { + isDirty = true + } + + @CallSuper + override fun endRecording() { + isDirty = false + } + + override fun isDirty(): Boolean { + return isDirty + } + + override fun isLocked(): Boolean { + return false + } + + override fun needRecord(): Boolean { + return isDirty() && !isLocked() + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt new file mode 100755 index 000000000..904d1bf56 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt @@ -0,0 +1,27 @@ +package io.legado.app.utils.canvasrecorder + +import android.graphics.Canvas + +interface CanvasRecorder { + + val width: Int + + val height: Int + + fun beginRecording(width: Int, height: Int): Canvas + + fun endRecording() + + fun draw(canvas: Canvas) + + fun invalidate() + + fun recycle() + + fun isDirty(): Boolean + + fun isLocked(): Boolean + + fun needRecord(): Boolean + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt new file mode 100755 index 000000000..bc0964f53 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt @@ -0,0 +1,47 @@ +package io.legado.app.utils.canvasrecorder + +import android.graphics.Canvas +import android.graphics.Picture +import io.legado.app.utils.canvasrecorder.pools.PicturePool +import io.legado.app.utils.objectpool.synchronized + +class CanvasRecorderApi23Impl : BaseCanvasRecorder() { + + private var picture: Picture? = null + + override val width get() = picture?.width ?: -1 + override val height get() = picture?.height ?: -1 + + private fun initPicture() { + if (picture == null) { + picture = picturePool.obtain() + } + } + + override fun beginRecording(width: Int, height: Int): Canvas { + initPicture() + return picture!!.beginRecording(width, height) + } + + override fun endRecording() { + picture!!.endRecording() + super.endRecording() + } + + override fun draw(canvas: Canvas) { + if (picture == null) return + canvas.drawPicture(picture!!) + } + + override fun recycle() { + super.recycle() + if (picture == null) return + picturePool.recycle(picture!!) + picture = null + } + + companion object { + private val picturePool = PicturePool().synchronized() + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt new file mode 100755 index 000000000..ce7205c88 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt @@ -0,0 +1,76 @@ +package io.legado.app.utils.canvasrecorder + +import android.graphics.Canvas +import android.graphics.Picture +import android.graphics.RenderNode +import android.os.Build +import androidx.annotation.RequiresApi +import io.legado.app.utils.objectpool.synchronized +import io.legado.app.utils.canvasrecorder.pools.PicturePool +import io.legado.app.utils.canvasrecorder.pools.RenderNodePool + +@RequiresApi(Build.VERSION_CODES.Q) +class CanvasRecorderApi29Impl : BaseCanvasRecorder() { + + private var renderNode: RenderNode? = null + private var picture: Picture? = null + + override val width get() = renderNode?.width ?: -1 + override val height get() = renderNode?.height ?: -1 + + private fun init() { + if (renderNode == null) { + renderNode = renderNodePool.obtain() + } + if (picture == null) { + picture = picturePool.obtain() + } + } + + private fun flushRenderNode() { + val rc = renderNode!!.beginRecording() + rc.drawPicture(picture!!) + renderNode!!.endRecording() + } + + override fun beginRecording(width: Int, height: Int): Canvas { + init() + renderNode!!.setPosition(0, 0, width, height) + return picture!!.beginRecording(width, height) + } + + override fun endRecording() { + picture!!.endRecording() + flushRenderNode() + super.endRecording() + } + + override fun draw(canvas: Canvas) { + if (renderNode == null || picture == null) { + return + } + if (canvas.isHardwareAccelerated) { + if (!renderNode!!.hasDisplayList()) { + flushRenderNode() + } + canvas.drawRenderNode(renderNode!!) + } else { + canvas.drawPicture(picture!!) + } + } + + override fun recycle() { + super.recycle() + if (renderNode == null || picture == null) return + renderNodePool.recycle(renderNode!!) + renderNode = null + picturePool.recycle(picture!!) + picture = null + } + + companion object { + private val picturePool = PicturePool().synchronized() + private val renderNodePool = RenderNodePool().synchronized() + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt new file mode 100755 index 000000000..17b905bf5 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt @@ -0,0 +1,44 @@ +package io.legado.app.utils.canvasrecorder + +import android.graphics.Canvas +import android.view.View +import androidx.core.graphics.withSave + +inline fun CanvasRecorder.recordIfNeeded( + width: Int, + height: Int, + block: Canvas.() -> Unit +): Boolean { + if (!needRecord()) return false + record(width, height, block) + return true +} + +fun CanvasRecorder.recordIfNeeded(view: View): Boolean { + if (!needRecord()) return false + record(view.width, view.height) { + view.draw(this) + } + return true +} + +inline fun CanvasRecorder.record(width: Int, height: Int, block: Canvas.() -> Unit) { + val canvas = beginRecording(width, height) + try { + canvas.withSave { + block() + } + } finally { + endRecording() + } +} + +inline fun CanvasRecorder.recordIfNeededThenDraw( + canvas: Canvas, + width: Int, + height: Int, + block: Canvas.() -> Unit +) { + recordIfNeeded(width, height, block) + draw(canvas) +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt new file mode 100755 index 000000000..f6c550372 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt @@ -0,0 +1,27 @@ +package io.legado.app.utils.canvasrecorder + +import android.os.Build +import io.legado.app.help.config.AppConfig + +object CanvasRecorderFactory { + + private val atLeastApi24 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + val isSupport = atLeastApi24 + + // issue 3868 + fun create(locked: Boolean = false): CanvasRecorder { + val impl = when { + !AppConfig.optimizeRender -> CanvasRecorderImpl() + atLeastApi29 -> CanvasRecorderApi29Impl() + atLeastApi24 -> CanvasRecorderApi23Impl() + else -> CanvasRecorderImpl() + } + return if (locked) { + CanvasRecorderLocked(impl) + } else { + impl + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt new file mode 100755 index 000000000..08bcd9ecd --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt @@ -0,0 +1,70 @@ +package io.legado.app.utils.canvasrecorder + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import com.bumptech.glide.Glide +import io.legado.app.utils.canvasrecorder.pools.CanvasPool +import splitties.init.appCtx + +class CanvasRecorderImpl : BaseCanvasRecorder() { + + var bitmap: Bitmap? = null + var canvas: Canvas? = null + + override val width get() = bitmap?.width ?: -1 + override val height get() = bitmap?.height ?: -1 + + private fun init(width: Int, height: Int) { + if (width <= 0 || height <= 0) { + return + } + if (bitmap == null) { + bitmap = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888) + } + if (bitmap!!.width != width || bitmap!!.height != height) { + if (bitmap!!.isMutable && canReconfigure(width, height)) { + bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888) + } else { + bitmapPool.put(bitmap!!) + bitmap = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888) + } + } + } + + private fun canReconfigure(width: Int, height: Int): Boolean { + return bitmap!!.allocationByteCount >= width * height * 4 + } + + override fun beginRecording(width: Int, height: Int): Canvas { + init(width, height) + bitmap?.eraseColor(Color.TRANSPARENT) + canvas = canvasPool.obtain().apply { setBitmap(bitmap) } + return canvas!! + } + + override fun endRecording() { + bitmap?.prepareToDraw() + super.endRecording() + canvasPool.recycle(canvas!!) + canvas = null + } + + override fun draw(canvas: Canvas) { + if (bitmap == null) return + canvas.drawBitmap(bitmap!!, 0f, 0f, null) + } + + override fun recycle() { + super.recycle() + val bitmap = bitmap ?: return + bitmapPool.put(bitmap) + this.bitmap = null + } + + companion object { + private val canvasPool = CanvasPool(2) + private val bitmapPool = Glide.get(appCtx).bitmapPool + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt new file mode 100755 index 000000000..e3b672e3e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt @@ -0,0 +1,64 @@ +package io.legado.app.utils.canvasrecorder + +import android.graphics.Canvas +import java.util.concurrent.locks.ReentrantLock + +class CanvasRecorderLocked(private val delegate: CanvasRecorder) : + CanvasRecorder by delegate { + + var lock: ReentrantLock? = ReentrantLock() + + private fun initLock() { + if (lock == null) { + synchronized(this) { + if (lock == null) { + lock = ReentrantLock() + } + } + } + } + + override fun beginRecording(width: Int, height: Int): Canvas { + initLock() + lock!!.lock() + return delegate.beginRecording(width, height) + } + + override fun endRecording() { + delegate.endRecording() + lock!!.unlock() + } + + override fun draw(canvas: Canvas) { + if (lock == null) { + return + } + lock!!.lock() + try { + delegate.draw(canvas) + } finally { + lock!!.unlock() + } + } + + override fun isLocked(): Boolean { + if (lock == null) { + return false + } + return lock!!.isLocked + } + + override fun recycle() { + if (lock == null) { + return + } + lock!!.lock() + try { + delegate.recycle() + } finally { + lock!!.unlock() + } + lock = null + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/CanvasPool.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/CanvasPool.kt new file mode 100755 index 000000000..2f3d625e7 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/CanvasPool.kt @@ -0,0 +1,21 @@ +package io.legado.app.utils.canvasrecorder.pools + +import android.graphics.Canvas +import androidx.core.util.Pools + +class CanvasPool(size: Int) { + + private val pool = Pools.SynchronizedPool(size) + + fun obtain(): Canvas { + val canvas = pool.acquire() ?: Canvas() + return canvas + } + + fun recycle(canvas: Canvas) { + canvas.setBitmap(null) + canvas.restoreToCount(1) + pool.release(canvas) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt new file mode 100755 index 000000000..cdf35774c --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt @@ -0,0 +1,10 @@ +package io.legado.app.utils.canvasrecorder.pools + +import android.graphics.Picture +import io.legado.app.utils.objectpool.BaseObjectPool + +class PicturePool : BaseObjectPool(64) { + + override fun create(): Picture = Picture() + +} diff --git a/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt b/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt new file mode 100755 index 000000000..b7a9e8ca0 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt @@ -0,0 +1,18 @@ +package io.legado.app.utils.canvasrecorder.pools + +import android.graphics.RenderNode +import android.os.Build +import androidx.annotation.RequiresApi +import io.legado.app.utils.objectpool.BaseObjectPool + +@RequiresApi(Build.VERSION_CODES.Q) +class RenderNodePool : BaseObjectPool(64) { + + override fun recycle(target: RenderNode) { + target.discardDisplayList() + super.recycle(target) + } + + override fun create(): RenderNode = RenderNode("CanvasRecorder") + +} diff --git a/app/src/main/java/io/legado/app/utils/compress/LibArchiveUtils.kt b/app/src/main/java/io/legado/app/utils/compress/LibArchiveUtils.kt new file mode 100755 index 000000000..d52719a86 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/compress/LibArchiveUtils.kt @@ -0,0 +1,372 @@ +package io.legado.app.utils.compress + +import android.os.ParcelFileDescriptor +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.system.OsConstants.S_ISDIR +import io.legado.app.lib.icu4j.CharsetDetector +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import okio.Buffer +import java.io.File +import java.io.FileDescriptor +import java.io.IOException +import java.io.InputStream +import java.io.InterruptedIOException +import java.nio.ByteBuffer +import java.nio.channels.SeekableByteChannel +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + + +object LibArchiveUtils { + + @Throws(ArchiveException::class) + fun openArchive( + inputStream: InputStream, + ): Long { + val archive: Long = Archive.readNew() + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readSetCallbackData(archive, null) + val buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback(archive) { _, _ -> + buffer.clear() + val bytesRead = try { + inputStream.read(buffer.array()) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.read", e) + } + if (bytesRead != -1) { + buffer.limit(bytesRead) + buffer + } else { + null + } + } + Archive.readSetSkipCallback(archive) { _, _, request -> + try { + inputStream.skip(request) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.skip", e) + } + } + Archive.readOpen1(archive) + successful = true + return archive + } finally { + if (!successful) { + Archive.free(archive) + } + } + + } + + + @Throws(ArchiveException::class) + private fun openArchive( + channel: SeekableByteChannel, + ): Long { + val archive: Long = Archive.readNew() + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readSetCallbackData(archive, null) + val buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback(archive) { _, _ -> + buffer.clear() + val bytesRead = try { + channel.read(buffer) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.read", e) + } + if (bytesRead != -1) { + buffer.flip() + buffer + } else { + null + } + } + Archive.readSetSkipCallback(archive) { _, _, request -> + try { + channel.position(channel.position() + request) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + } + request + } + Archive.readSetSeekCallback(archive) { _, _, offset, whence -> + val newPosition: Long + try { + newPosition = when (whence) { + OsConstants.SEEK_SET -> offset + OsConstants.SEEK_CUR -> channel.position() + offset + OsConstants.SEEK_END -> channel.size() + offset + else -> throw ArchiveException( + Archive.ERRNO_FATAL, + "Unknown whence $whence" + ) + } + channel.position(newPosition) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + } + newPosition + } + Archive.readOpen1(archive) + successful = true + return archive + + + } finally { + if (!successful) { + Archive.free(archive) + } + } + + } + + + @Throws(ArchiveException::class) + private fun openArchive( + pfd: ParcelFileDescriptor, + useCb: Boolean = true + ): Long { + val archive: Long = Archive.readNew() + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + if (useCb) { + Archive.readSetCallbackData(archive, pfd.fileDescriptor) + val buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback( + archive + ) { _: Long, fd: Any? -> + buffer.clear() + try { + Os.read(fd as FileDescriptor?, buffer) + } catch (e: ErrnoException) { + throw ArchiveException(Archive.ERRNO_FATAL, "Os.read", e) + } catch (e: InterruptedIOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "Os.read", e) + } + buffer.flip() + buffer + } + Archive.readSetSkipCallback( + archive + ) { _: Long, fd: Any?, request: Long -> + try { + Os.lseek( + fd as FileDescriptor?, request, OsConstants.SEEK_CUR + ) + } catch (e: ErrnoException) { + throw ArchiveException(Archive.ERRNO_FATAL, "Os.lseek", e) + } + request + } + Archive.readSetSeekCallback( + archive + ) { _: Long, fd: Any?, offset: Long, whence: Int -> + try { + return@readSetSeekCallback Os.lseek( + fd as FileDescriptor?, offset, whence + ) + } catch (e: ErrnoException) { + throw ArchiveException(Archive.ERRNO_FATAL, "Os.lseek", e) + } + } + Archive.readOpen1(archive) + + } else { + Archive.readOpenFd(archive, pfd.fd, DEFAULT_BUFFER_SIZE.toLong()) + } + + successful = true + return archive + + + } finally { + if (!successful) { + Archive.free(archive) + } + } + + + } + + /** + * 解压文件 + */ + @Throws(NullPointerException::class, SecurityException::class) + fun unArchive( + pfd: ParcelFileDescriptor, + destDir: File, + filter: ((String) -> Boolean)? = null + ): List { + return unArchive(openArchive(pfd), destDir, filter) + } + + /** + * 解压 + */ + @Throws(NullPointerException::class, SecurityException::class) + private fun unArchive( + archive: Long, + destDir: File?, + filter: ((String) -> Boolean)? = null + ): List { + destDir ?: throw NullPointerException("解压路径不能为空") + val files = arrayListOf() + + + try { + var entry: Long + + while (Archive.readNextHeader(archive).also { entry = it } != 0L) { + val entryName = + getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry)) + ?: continue + val entryFile = File(destDir, entryName) + if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath)) { + throw SecurityException("压缩文件只能解压到指定路径") + } + val entryStat = ArchiveEntry.stat(entry) + + //判断是否是文件夹 + if (entryStat.isDir()) { + if (!entryFile.exists()) { + entryFile.mkdirs() + } + continue + } + + + + if (entryFile.parentFile?.exists() != true) { + entryFile.parentFile?.mkdirs() + } + if (filter != null && !filter.invoke(entryName)) continue + if (!entryFile.exists()) { + entryFile.createNewFile() + entryFile.setReadable(true) + entryFile.setExecutable(true) + } + + ParcelFileDescriptor.open(entryFile, ParcelFileDescriptor.MODE_WRITE_ONLY).use { + Archive.readDataIntoFd(archive, it.fd) + files.add(entryFile) + } + + + } + } finally { + Archive.free(archive) + } + + return files + + } + + fun getFilesName(pfd: ParcelFileDescriptor, filter: ((String) -> Boolean)?): List { + return getFilesName(openArchive(pfd), filter) + } + + fun getByteArrayContent(inputStream: InputStream, path: String): ByteArray? { + val archive = openArchive(inputStream) + try { + var entry: Long + while (Archive.readNextHeader(archive).also { entry = it } != 0L) { + val entryName = + getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry)) + ?: continue + + val entryStat = ArchiveEntry.stat(entry) + + //判断是否是文件夹 + if (entryStat.isDir()) { + continue + } + + if (entryName == path) { + val byteBuffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE) + val buffer = Buffer() + while (true) { + Archive.readData(archive, byteBuffer) + byteBuffer.flip() + if (!byteBuffer.hasRemaining()) { + return buffer.readByteArray() + } + buffer.write(byteBuffer) + byteBuffer.clear() + } + } + + + } + } finally { + Archive.free(archive) + } + return null + } + + @Throws(SecurityException::class) + private fun getFilesName( + archive: Long, + filter: ((String) -> Boolean)? = null + ): List { + val fileNames = mutableListOf() + try { + + + var entry: Long + + while (Archive.readNextHeader(archive).also { entry = it } != 0L) { + val fileName = + getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry)) + ?: continue + + + val entryStat = ArchiveEntry.stat(entry) + + if (entryStat.isDir()) { + continue + } + + if (filter != null && filter.invoke(fileName)) + fileNames.add(fileName) + + + } + } finally { + Archive.free(archive) + } + + return fileNames + } + + private fun ArchiveEntry.StructStat.isDir() = S_ISDIR(this.stMode) + + private fun getEntryString(utf8: String?, bytes: ByteArray?): String? { + return utf8 ?: newStringFromBytes(bytes) + } + + private fun newStringFromBytes(bytes: ByteArray?): String? { + bytes ?: return null + val cd = CharsetDetector() + cd.setText(bytes) + val c = cd.detectAll().first().name + return String(bytes, Charset.forName(c)) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt b/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt new file mode 100755 index 000000000..d84f54f94 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt @@ -0,0 +1,410 @@ +package io.legado.app.utils.compress + +import android.annotation.SuppressLint +import io.legado.app.utils.DebugLog +import io.legado.app.utils.compress.ZipUtils.zipFile +import io.legado.app.utils.printOnDebug +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import java.io.BufferedInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.util.zip.GZIPOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +@SuppressLint("ObsoleteSdkInt") +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ZipUtils { + + fun gzipByteArray(byteArray: ByteArray): ByteArray { + val byteOut = ByteArrayOutputStream() + val zip = GZIPOutputStream(byteOut) + return zip.use { + it.write(byteArray) + byteOut.use { + byteOut.toByteArray() + } + } + } + + fun zipByteArray(byteArray: ByteArray, fileName: String): ByteArray { + val byteOut = ByteArrayOutputStream() + val zipOutputStream = ZipOutputStream(byteOut) + zipOutputStream.putNextEntry(ZipEntry(fileName)) + zipOutputStream.write(byteArray) + zipOutputStream.closeEntry() + zipOutputStream.finish() + return zipOutputStream.use { + byteOut.use { + byteOut.toByteArray() + } + } + } + + /** + * Zip the files. + * + * @param srcFiles The source of files. + * @param zipFilePath The path of ZIP file. + * @return `true`: success

    `false`: fail + * @throws IOException if an I/O error has occurred + */ + suspend fun zipFiles( + srcFiles: Collection, + zipFilePath: String + ): Boolean { + return zipFiles(srcFiles, zipFilePath, null) + } + + /** + * Zip the files. + * + * @param srcFilePaths The paths of source files. + * @param zipFilePath The path of ZIP file. + * @param comment The comment. + * @return `true`: success

    `false`: fail + * @throws IOException if an I/O error has occurred + */ + suspend fun zipFiles( + srcFilePaths: Collection?, + zipFilePath: String?, + comment: String? + ): Boolean = withContext(IO) { + if (srcFilePaths == null || zipFilePath == null) return@withContext false + ZipOutputStream(FileOutputStream(zipFilePath)).use { + for (srcFile in srcFilePaths) { + if (!zipFile(getFileByPath(srcFile)!!, "", it, comment)) + return@withContext false + } + return@withContext true + } + } + + /** + * Zip the files. + * + * @param srcFiles The source of files. + * @param zipFile The ZIP file. + * @param comment The comment. + * @return `true`: success

    `false`: fail + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + @JvmOverloads + fun zipFiles( + srcFiles: Collection?, + zipFile: File?, + comment: String? = null + ): Boolean { + if (srcFiles == null || zipFile == null) return false + ZipOutputStream(FileOutputStream(zipFile)).use { + for (srcFile in srcFiles) { + if (!zipFile(srcFile, "", it, comment)) return false + } + return true + } + } + + /** + * Zip the file. + * + * @param srcFilePath The path of source file. + * @param zipFilePath The path of ZIP file. + * @return `true`: success

    `false`: fail + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + fun zipFile( + srcFilePath: String, + zipFilePath: String + ): Boolean { + return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null) + } + + /** + * Zip the file. + * + * @param srcFilePath The path of source file. + * @param zipFilePath The path of ZIP file. + * @param comment The comment. + * @return `true`: success

    `false`: fail + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + fun zipFile( + srcFilePath: String, + zipFilePath: String, + comment: String + ): Boolean { + return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment) + } + + /** + * Zip the file. + * + * @param srcFile The source of file. + * @param zipFile The ZIP file. + * @param comment The comment. + * @return `true`: success

    `false`: fail + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + @JvmOverloads + fun zipFile( + srcFile: File?, + zipFile: File?, + comment: String? = null + ): Boolean { + if (srcFile == null || zipFile == null) return false + ZipOutputStream(FileOutputStream(zipFile)).use { zos -> + return zipFile(srcFile, "", zos, comment) + } + } + + @Throws(IOException::class) + private fun zipFile( + srcFile: File, + rootPath: String, + zos: ZipOutputStream, + comment: String? + ): Boolean { + var rootPath1 = rootPath + if (!srcFile.exists()) return true + rootPath1 = rootPath1 + (if (isSpace(rootPath1)) "" else File.separator) + srcFile.name + if (srcFile.isDirectory) { + val fileList = srcFile.listFiles() + if (fileList == null || fileList.isEmpty()) { + val entry = ZipEntry("$rootPath1/") + entry.comment = comment + zos.putNextEntry(entry) + zos.closeEntry() + } else { + for (file in fileList) { + if (!zipFile(file, rootPath1, zos, comment)) return false + } + } + } else { + BufferedInputStream(FileInputStream(srcFile)).use { + val entry = ZipEntry(rootPath1) + entry.comment = comment + zos.putNextEntry(entry) + it.copyTo(zos) + zos.closeEntry() + } + } + return true + } + + @Throws(SecurityException::class) + fun unZipToPath(file: File, path: String, filter: ((String) -> Boolean)? = null): List { + return FileInputStream(file).use { + unZipToPath(it, path, filter) + } + } + + @Throws(SecurityException::class) + fun unZipToPath(file: File, dir: File, filter: ((String) -> Boolean)? = null): List { + return FileInputStream(file).use { + unZipToPath(it, dir, filter) + } + } + + @Throws(SecurityException::class) + fun unZipToPath( + inputStream: InputStream, + path: String, + filter: ((String) -> Boolean)? = null + ): List { + return ZipInputStream(inputStream).use { + unZipToPath(it, File(path), filter) + } + } + + @Throws(SecurityException::class) + fun unZipToPath( + inputStream: InputStream, + dir: File, + filter: ((String) -> Boolean)? = null + ): List { + return ZipInputStream(inputStream).use { + unZipToPath(it, dir, filter) + } + } + + @Throws(SecurityException::class) + private fun unZipToPath( + zipInputStream: ZipInputStream, + dir: File, + filter: ((String) -> Boolean)? = null + ): List { + val files = arrayListOf() + var entry: ZipEntry? + while (zipInputStream.nextEntry.also { entry = it } != null) { + val entryName = entry!!.name + val entryFile = File(dir, entryName) + if (!entryFile.canonicalPath.startsWith(dir.canonicalPath)) { + throw SecurityException("压缩文件只能解压到指定路径") + } + if (entry.isDirectory) { + if (!entryFile.exists()) { + entryFile.mkdirs() + } + continue + } + if (entryFile.parentFile?.exists() != true) { + entryFile.parentFile?.mkdirs() + } + if (filter != null && !filter.invoke(entryName)) continue + if (!entryFile.exists()) { + entryFile.createNewFile() + entryFile.setReadable(true) + entryFile.setExecutable(true) + } + FileOutputStream(entryFile).use { + zipInputStream.copyTo(it) + files.add(entryFile) + } + } + return files + } + + /* 遍历目录获取所有文件名 */ + @Throws(SecurityException::class) + fun getFilesName( + inputStream: InputStream, + filter: ((String) -> Boolean)? = null + ): List { + return ZipInputStream(inputStream).use { + getFilesName(it, filter) + } + } + + @Throws(SecurityException::class) + private fun getFilesName( + zipInputStream: ZipInputStream, + filter: ((String) -> Boolean)? = null + ): List { + val fileNames = mutableListOf() + var entry: ZipEntry? + while (zipInputStream.nextEntry.also { entry = it } != null) { + if (entry!!.isDirectory) { + continue + } + val fileName = entry.name + if (filter != null && filter.invoke(fileName)) + fileNames.add(fileName) + } + return fileNames + } + + /** + * Return the files' path in ZIP file. + * + * @param zipFilePath The path of ZIP file. + * @return the files' path in ZIP file + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + fun getFilesPath(zipFilePath: String): List? { + return getFilesPath(getFileByPath(zipFilePath)) + } + + /** + * Return the files' path in ZIP file. + * + * @param zipFile The ZIP file. + * @return the files' path in ZIP file + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + fun getFilesPath(zipFile: File?): List? { + if (zipFile == null) return null + val paths = ArrayList() + val zip = ZipFile(zipFile) + val entries = zip.entries() + while (entries.hasMoreElements()) { + val entryName = (entries.nextElement() as ZipEntry).name + if (entryName.contains("../")) { + DebugLog.e(javaClass.name, "entryName: $entryName is dangerous!") + paths.add(entryName) + } else { + paths.add(entryName) + } + } + zip.close() + return paths + } + + /** + * Return the files' comment in ZIP file. + * + * @param zipFilePath The path of ZIP file. + * @return the files' comment in ZIP file + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + fun getComments(zipFilePath: String): List? { + return getComments(getFileByPath(zipFilePath)) + } + + /** + * Return the files' comment in ZIP file. + * + * @param zipFile The ZIP file. + * @return the files' comment in ZIP file + * @throws IOException if an I/O error has occurred + */ + @Throws(IOException::class) + fun getComments(zipFile: File?): List? { + if (zipFile == null) return null + val comments = ArrayList() + val zip = ZipFile(zipFile) + val entries = zip.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() as ZipEntry + comments.add(entry.comment) + } + zip.close() + return comments + } + + private fun createOrExistsDir(file: File?): Boolean { + return file != null && if (file.exists()) file.isDirectory else file.mkdirs() + } + + private fun createOrExistsFile(file: File?): Boolean { + if (file == null) return false + if (file.exists()) return file.isFile + if (!createOrExistsDir(file.parentFile)) return false + return try { + file.createNewFile() + } catch (e: IOException) { + e.printOnDebug() + false + } + } + + private fun getFileByPath(filePath: String): File? { + return if (isSpace(filePath)) null else File(filePath) + } + + private fun isSpace(s: String?): Boolean { + if (s == null) return true + var i = 0 + val len = s.length + while (i < len) { + if (!Character.isWhitespace(s[i])) { + return false + } + ++i + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt b/app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt new file mode 100755 index 000000000..b40201f70 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt @@ -0,0 +1,19 @@ +package io.legado.app.utils.objectpool + +import androidx.annotation.CallSuper +import androidx.core.util.Pools + +abstract class BaseObjectPool(size: Int) : ObjectPool { + + open val pool = Pools.SimplePool(size) + + override fun obtain(): T { + return pool.acquire() ?: create() + } + + @CallSuper + override fun recycle(target: T) { + pool.release(target) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt b/app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt new file mode 100755 index 000000000..d5fb1eca6 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt @@ -0,0 +1,9 @@ +package io.legado.app.utils.objectpool + +import androidx.core.util.Pools + +abstract class BaseSafeObjectPool(size: Int): BaseObjectPool(size) { + + override val pool = Pools.SynchronizedPool(size) + +} diff --git a/app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt b/app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt new file mode 100755 index 000000000..f63a14d79 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt @@ -0,0 +1,11 @@ +package io.legado.app.utils.objectpool + +interface ObjectPool { + + fun obtain(): T + + fun recycle(target: T) + + fun create(): T + +} diff --git a/app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt b/app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt new file mode 100755 index 000000000..a38c4d521 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt @@ -0,0 +1,3 @@ +package io.legado.app.utils.objectpool + +fun ObjectPool.synchronized(): ObjectPool = ObjectPoolLocked(this) diff --git a/app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt b/app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt new file mode 100755 index 000000000..890067b6e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt @@ -0,0 +1,15 @@ +package io.legado.app.utils.objectpool + +class ObjectPoolLocked(private val delegate: ObjectPool) : ObjectPool by delegate { + + @Synchronized + override fun obtain(): T { + return delegate.obtain() + } + + @Synchronized + override fun recycle(target: T) { + return delegate.recycle(target) + } + +} diff --git a/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt new file mode 100755 index 000000000..321181022 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ActivityViewBindings.kt @@ -0,0 +1,22 @@ +@file:Suppress("RedundantVisibilityModifier", "unused") + +package io.legado.app.utils.viewbindingdelegate + +import android.view.LayoutInflater +import androidx.core.app.ComponentActivity +import androidx.viewbinding.ViewBinding + +/** + * Create new [ViewBinding] associated with the [ComponentActivity] + */ +@JvmName("viewBindingActivity") +inline fun ComponentActivity.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T, + setContentView: Boolean = false +) = lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + val binding = bindingInflater.invoke(layoutInflater) + if (setContentView) { + setContentView(binding.root) + } + binding +} diff --git a/app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt new file mode 100755 index 000000000..fb713cd66 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/FragmentViewBindings.kt @@ -0,0 +1,54 @@ +@file:Suppress("RedundantVisibilityModifier", "unused", "UnusedReceiverParameter") +@file:JvmName("ReflectionFragmentViewBindings") + +package io.legado.app.utils.viewbindingdelegate + +import android.view.View +import androidx.annotation.IdRes +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding + +private class FragmentViewBindingProperty( + viewBinder: (F) -> T +) : ViewBindingProperty(viewBinder) { + + override fun getLifecycleOwner(thisRef: F) = thisRef.viewLifecycleOwner +} + +/** + * Create new [ViewBinding] associated with the [Fragment] + */ +@JvmName("viewBindingFragment") +public fun Fragment.viewBinding(viewBinder: (F) -> T): ViewBindingProperty { + return FragmentViewBindingProperty(viewBinder) +} + +/** + * Create new [ViewBinding] associated with the [Fragment] + * + * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used + * @param viewProvider Provide a [View] from the Fragment. By default call [Fragment.requireView] + */ +@JvmName("viewBindingFragment") +public inline fun Fragment.viewBinding( + crossinline vbFactory: (View) -> T, + crossinline viewProvider: (F) -> View = Fragment::requireView +): ViewBindingProperty { + return viewBinding { fragment: F -> vbFactory(viewProvider(fragment)) } +} + +/** + * Create new [ViewBinding] associated with the [Fragment] + * + * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used + * @param viewBindingRootId Root view's id that will be used as root for the view binding + */ +@JvmName("viewBindingFragment") +public inline fun Fragment.viewBinding( + crossinline vbFactory: (View) -> T, + @IdRes viewBindingRootId: Int +): ViewBindingProperty { + return viewBinding(vbFactory) { fragment: Fragment -> + fragment.requireView().findViewById(viewBindingRootId) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt new file mode 100755 index 000000000..10a23ce3c --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/viewbindingdelegate/ViewBindingProperty.kt @@ -0,0 +1,57 @@ +@file:Suppress("RedundantVisibilityModifier") + +package io.legado.app.utils.viewbindingdelegate + +import android.os.Handler +import android.os.Looper +import androidx.annotation.MainThread +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +public abstract class ViewBindingProperty( + private val viewBinder: (R) -> T +) : ReadOnlyProperty { + + private var viewBinding: T? = null + private val lifecycleObserver = ClearOnDestroyLifecycleObserver() + private var thisRef: R? = null + + protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner + + @MainThread + public override fun getValue(thisRef: R, property: KProperty<*>): T { + viewBinding?.let { return it } + + this.thisRef = thisRef + val lifecycle = getLifecycleOwner(thisRef).lifecycle + if (lifecycle.currentState == Lifecycle.State.DESTROYED) { + mainHandler.post { viewBinding = null } + } else { + lifecycle.addObserver(lifecycleObserver) + } + return viewBinder(thisRef).also { viewBinding = it } + } + + @MainThread + public fun clear() { + val thisRef = thisRef ?: return + this.thisRef = null + getLifecycleOwner(thisRef).lifecycle.removeObserver(lifecycleObserver) + mainHandler.post { viewBinding = null } + } + + private inner class ClearOnDestroyLifecycleObserver : DefaultLifecycleObserver { + + @MainThread + override fun onDestroy(owner: LifecycleOwner): Unit = clear() + } + + private companion object { + + private val mainHandler = Handler(Looper.getMainLooper()) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/web/HttpServer.kt b/app/src/main/java/io/legado/app/web/HttpServer.kt new file mode 100755 index 000000000..201aa17d6 --- /dev/null +++ b/app/src/main/java/io/legado/app/web/HttpServer.kt @@ -0,0 +1,152 @@ +package io.legado.app.web + +import android.graphics.Bitmap +import fi.iki.elonen.NanoHTTPD +import io.legado.app.api.ReturnData +import io.legado.app.api.controller.BookController +import io.legado.app.api.controller.BookSourceController +import io.legado.app.api.controller.ReplaceRuleController +import io.legado.app.api.controller.RssSourceController +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.service.WebService +import io.legado.app.utils.GSON +import io.legado.app.utils.LogUtils +import io.legado.app.utils.stackTraceStr +import io.legado.app.web.utils.AssetsWeb +import kotlinx.coroutines.runBlocking +import okio.Pipe +import okio.buffer +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +class HttpServer(port: Int) : NanoHTTPD(port) { + private val assetsWeb = AssetsWeb("web") + + override fun serve(session: IHTTPSession): Response { + WebService.serve() + var returnData: ReturnData? = null + val ct = ContentType(session.headers["content-type"]).tryUTF8() + session.headers["content-type"] = ct.contentTypeHeader + var uri = session.uri + + val startAt = System.currentTimeMillis() + LogUtils.d(TAG) { + "${session.method.name} - $uri - ${session.queryParameterString} - Start($startAt)" + } + + try { + when (session.method) { + Method.OPTIONS -> { + val response = newFixedLengthResponse("") + response.addHeader("Access-Control-Allow-Methods", "POST") + response.addHeader("Access-Control-Allow-Headers", "content-type") + response.addHeader("Access-Control-Allow-Origin", session.headers["origin"]) + //response.addHeader("Access-Control-Max-Age", "3600"); + return response + } + + Method.POST -> { + val files = HashMap() + session.parseBody(files) + val postData = files["postData"] + + returnData = runBlocking { + when (uri) { + "/saveBookSource" -> BookSourceController.saveSource(postData) + "/saveBookSources" -> BookSourceController.saveSources(postData) + "/deleteBookSources" -> BookSourceController.deleteSources(postData) + "/saveBook" -> BookController.saveBook(postData) + "/deleteBook" -> BookController.deleteBook(postData) + "/saveBookProgress" -> BookController.saveBookProgress(postData) + "/addLocalBook" -> BookController.addLocalBook(session.parameters, files) + "/saveReadConfig" -> BookController.saveWebReadConfig(postData) + "/saveRssSource" -> RssSourceController.saveSource(postData) + "/saveRssSources" -> RssSourceController.saveSources(postData) + "/deleteRssSources" -> RssSourceController.deleteSources(postData) + "/saveReplaceRule" -> ReplaceRuleController.saveRule(postData) + "/deleteReplaceRule" -> ReplaceRuleController.delete(postData) + "/testReplaceRule" -> ReplaceRuleController.testRule(postData) + else -> null + } + } + } + + Method.GET -> { + val parameters = session.parameters + + returnData = when (uri) { + "/getBookSource" -> BookSourceController.getSource(parameters) + "/getBookSources" -> BookSourceController.sources + "/getBookshelf" -> BookController.bookshelf + "/getChapterList" -> BookController.getChapterList(parameters) + "/refreshToc" -> BookController.refreshToc(parameters) + "/getBookContent" -> BookController.getBookContent(parameters) + "/cover" -> BookController.getCover(parameters) + "/image" -> BookController.getImg(parameters) + "/getReadConfig" -> BookController.getWebReadConfig() + "/getRssSource" -> RssSourceController.getSource(parameters) + "/getRssSources" -> RssSourceController.sources + "/getReplaceRules" -> ReplaceRuleController.allRules + else -> null + } + } + + else -> Unit + } + + if (returnData == null) { + if (uri.endsWith("/")) + uri += "index.html" + return assetsWeb.getResponse(uri) + } + + val response = if (returnData.data is Bitmap) { + val outputStream = ByteArrayOutputStream() + (returnData.data as Bitmap).compress(Bitmap.CompressFormat.PNG, 100, outputStream) + val byteArray = outputStream.toByteArray() + outputStream.close() + val inputStream = ByteArrayInputStream(byteArray) + newFixedLengthResponse( + Response.Status.OK, + "image/png", + inputStream, + byteArray.size.toLong() + ) + } else { + val data = returnData.data + if (data is List<*> && data.size > 3000) { + val pipe = Pipe(16 * 1024) + Coroutine.async { + pipe.sink.buffer().outputStream().bufferedWriter(Charsets.UTF_8).use { + GSON.toJson(returnData, it) + } + } + newChunkedResponse( + Response.Status.OK, + "application/json", + pipe.source.buffer().inputStream() + ) + } else { + newFixedLengthResponse(GSON.toJson(returnData)) + } + } + response.addHeader("Access-Control-Allow-Methods", "GET, POST") + response.addHeader("Access-Control-Allow-Origin", session.headers["origin"]) + LogUtils.d(TAG) { + "${session.method.name} - $uri - ${session.queryParameterString} - End($startAt)" + } + return response + } catch (e: Exception) { + LogUtils.d(TAG) { + "${session.method.name} - $uri - ${session.queryParameterString} - Error End($startAt)\n$e\n${e.stackTraceStr}" + } + return newFixedLengthResponse(e.message) + } + + } + + companion object { + private const val TAG = "HttpServer" + } + +} diff --git a/app/src/main/java/io/legado/app/web/ReadMe.md b/app/src/main/java/io/legado/app/web/ReadMe.md new file mode 100755 index 000000000..aee2e2dc6 --- /dev/null +++ b/app/src/main/java/io/legado/app/web/ReadMe.md @@ -0,0 +1,5 @@ +# web服务 + +* controller 数据操作 +* HttpServer http服务 +* WebSocketServer 持续通讯服务 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/web/WebSocketServer.kt b/app/src/main/java/io/legado/app/web/WebSocketServer.kt new file mode 100755 index 000000000..f2c7c27a0 --- /dev/null +++ b/app/src/main/java/io/legado/app/web/WebSocketServer.kt @@ -0,0 +1,24 @@ +package io.legado.app.web + +import fi.iki.elonen.NanoWSD +import io.legado.app.service.WebService +import io.legado.app.web.socket.* + +class WebSocketServer(port: Int) : NanoWSD(port) { + + override fun openWebSocket(handshake: IHTTPSession): WebSocket? { + WebService.serve() + return when (handshake.uri) { + "/bookSourceDebug" -> { + BookSourceDebugWebSocket(handshake) + } + "/rssSourceDebug" -> { + RssSourceDebugWebSocket(handshake) + } + "/searchBook" -> { + BookSearchWebSocket(handshake) + } + else -> null + } + } +} diff --git a/app/src/main/java/io/legado/app/web/socket/BookSearchWebSocket.kt b/app/src/main/java/io/legado/app/web/socket/BookSearchWebSocket.kt new file mode 100755 index 000000000..c6917e410 --- /dev/null +++ b/app/src/main/java/io/legado/app/web/socket/BookSearchWebSocket.kt @@ -0,0 +1,97 @@ +package io.legado.app.web.socket + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoWSD +import io.legado.app.R +import io.legado.app.data.entities.SearchBook +import io.legado.app.help.config.AppConfig +import io.legado.app.model.webBook.SearchModel +import io.legado.app.ui.book.search.SearchScope +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.isJson +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import splitties.init.appCtx +import java.io.IOException + +class BookSearchWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : + NanoWSD.WebSocket(handshakeRequest), + CoroutineScope by MainScope(), + SearchModel.CallBack { + + private val normalClosure = NanoWSD.WebSocketFrame.CloseCode.NormalClosure + private val searchModel = SearchModel(this, this) + + private val SEARCH_FINISH = "Search finish" + + override fun onOpen() { + launch(IO) { + kotlin.runCatching { + while (isOpen) { + ping("ping".toByteArray()) + delay(30000) + } + } + } + } + + override fun onClose( + code: NanoWSD.WebSocketFrame.CloseCode, + reason: String, + initiatedByRemote: Boolean + ) { + cancel() + searchModel.close() + } + + override fun onMessage(message: NanoWSD.WebSocketFrame) { + launch(IO) { + kotlin.runCatching { + if (!message.textPayload.isJson()) { + send("数据必须为Json格式") + close(normalClosure, SEARCH_FINISH, false) + return@launch + } + val searchMap = + GSON.fromJsonObject>(message.textPayload).getOrNull() + if (searchMap != null) { + val key = searchMap["key"] + if (key.isNullOrBlank()) { + send(appCtx.getString(R.string.cannot_empty)) + close(normalClosure, SEARCH_FINISH, false) + return@launch + } + searchModel.search(System.currentTimeMillis(), key) + } + } + } + } + + override fun onPong(pong: NanoWSD.WebSocketFrame) { + + } + + override fun onException(exception: IOException) { + + } + + override fun getSearchScope(): SearchScope = SearchScope(AppConfig.searchScope) + + override fun onSearchStart() { + + } + + override fun onSearchSuccess(searchBooks: List) { + send(GSON.toJson(searchBooks)) + } + + override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) = close(normalClosure, SEARCH_FINISH, false) + + override fun onSearchCancel(exception: Throwable?) = close(normalClosure, exception?.toString() ?: SEARCH_FINISH, false) + +} diff --git a/app/src/main/java/io/legado/app/web/socket/BookSourceDebugWebSocket.kt b/app/src/main/java/io/legado/app/web/socket/BookSourceDebugWebSocket.kt new file mode 100755 index 000000000..1e158399d --- /dev/null +++ b/app/src/main/java/io/legado/app/web/socket/BookSourceDebugWebSocket.kt @@ -0,0 +1,102 @@ +package io.legado.app.web.socket + + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoWSD +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.model.Debug +import io.legado.app.utils.* +import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers.IO +import splitties.init.appCtx + +import java.io.IOException + +/** + * web端书源调试 + */ +class BookSourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : + NanoWSD.WebSocket(handshakeRequest), + CoroutineScope by MainScope(), + Debug.Callback { + + private val notPrintState = arrayOf(10, 20, 30, 40) + + override fun onOpen() { + launch(IO) { + kotlin.runCatching { + while (isOpen) { + ping("ping".toByteArray()) + delay(30000) + } + } + } + } + + override fun onClose( + code: NanoWSD.WebSocketFrame.CloseCode, + reason: String, + initiatedByRemote: Boolean + ) { + cancel() + Debug.cancelDebug(true) + } + + override fun onMessage(message: NanoWSD.WebSocketFrame) { + launch(IO) { + kotlin.runCatching { + if (!message.textPayload.isJson()) { + send("数据必须为Json格式") + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + val debugBean = + GSON.fromJsonObject>(message.textPayload).getOrNull() + if (debugBean != null) { + val tag = debugBean["tag"] + val key = debugBean["key"] + if (tag.isNullOrBlank() || key.isNullOrBlank()) { + send(appCtx.getString(R.string.cannot_empty)) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + appDb.bookSourceDao.getBookSource(tag)?.let { + Debug.callback = this@BookSourceDebugWebSocket + Debug.startDebug(this, it, key) + } + } else { + send("数据必须为Json格式") + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + } + } + } + + override fun onPong(pong: NanoWSD.WebSocketFrame) { + + } + + override fun onException(exception: IOException) { + Debug.cancelDebug(true) + } + + override fun printLog(state: Int, msg: String) { + if (state in notPrintState) { + return + } + runOnIO { + runCatching { + send(msg) + if (state == -1 || state == 1000) { + Debug.cancelDebug(true) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + } + }.onFailure { + it.printOnDebug() + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/web/socket/RssSourceDebugWebSocket.kt b/app/src/main/java/io/legado/app/web/socket/RssSourceDebugWebSocket.kt new file mode 100755 index 000000000..6eae0d39b --- /dev/null +++ b/app/src/main/java/io/legado/app/web/socket/RssSourceDebugWebSocket.kt @@ -0,0 +1,97 @@ +package io.legado.app.web.socket + + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoWSD +import io.legado.app.R +import io.legado.app.data.appDb +import io.legado.app.model.Debug +import io.legado.app.utils.* +import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers.IO +import splitties.init.appCtx + +import java.io.IOException + +/** + * web端订阅源调试 + */ +class RssSourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : + NanoWSD.WebSocket(handshakeRequest), + CoroutineScope by MainScope(), + Debug.Callback { + + private val notPrintState = arrayOf(10, 20, 30, 40) + + override fun onOpen() { + launch(IO) { + kotlin.runCatching { + while (isOpen) { + ping("ping".toByteArray()) + delay(30000) + } + } + } + } + + override fun onClose( + code: NanoWSD.WebSocketFrame.CloseCode, + reason: String, + initiatedByRemote: Boolean + ) { + cancel() + Debug.cancelDebug(true) + } + + override fun onMessage(message: NanoWSD.WebSocketFrame) { + launch(IO) { + kotlin.runCatching { + if (!message.textPayload.isJson()) { + send("数据必须为Json格式") + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + val debugBean = + GSON.fromJsonObject>(message.textPayload).getOrNull() + if (debugBean != null) { + val tag = debugBean["tag"] + if (tag.isNullOrBlank()) { + send(appCtx.getString(R.string.cannot_empty)) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + appDb.rssSourceDao.getByKey(tag)?.let { + Debug.callback = this@RssSourceDebugWebSocket + Debug.startDebug(this, it) + } + } + } + } + } + + override fun onPong(pong: NanoWSD.WebSocketFrame) { + + } + + override fun onException(exception: IOException) { + Debug.cancelDebug(true) + } + + override fun printLog(state: Int, msg: String) { + if (state in notPrintState) { + return + } + runOnIO { + runCatching { + send(msg) + if (state == -1 || state == 1000) { + Debug.cancelDebug(true) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + } + }.onFailure { + it.printOnDebug() + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt b/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt new file mode 100755 index 000000000..2e07979f5 --- /dev/null +++ b/app/src/main/java/io/legado/app/web/utils/AssetsWeb.kt @@ -0,0 +1,45 @@ +package io.legado.app.web.utils + +import android.content.res.AssetManager +import android.text.TextUtils +import fi.iki.elonen.NanoHTTPD +import splitties.init.appCtx +import java.io.File +import java.io.IOException + + +class AssetsWeb(rootPath: String) { + private val assetManager: AssetManager = appCtx.assets + private var rootPath = "web" + + init { + if (!TextUtils.isEmpty(rootPath)) { + this.rootPath = rootPath + } + } + + @Throws(IOException::class) + fun getResponse(path: String): NanoHTTPD.Response { + var path1 = path + path1 = (rootPath + path1).replace("/+".toRegex(), File.separator) + val inputStream = assetManager.open(path1) + return NanoHTTPD.newChunkedResponse( + NanoHTTPD.Response.Status.OK, + getMimeType(path1), + inputStream + ) + } + + private fun getMimeType(path: String): String { + val suffix = path.substring(path.lastIndexOf(".")) + return when { + suffix.equals(".html", ignoreCase = true) + || suffix.equals(".htm", ignoreCase = true) -> "text/html" + suffix.equals(".js", ignoreCase = true) -> "text/javascript" + suffix.equals(".css", ignoreCase = true) -> "text/css" + suffix.equals(".ico", ignoreCase = true) -> "image/x-icon" + suffix.equals(".jpg", ignoreCase = true) -> "image/jpg" + else -> "text/html" + } + } +} diff --git a/app/src/main/res/anim/anim_none.xml b/app/src/main/res/anim/anim_none.xml new file mode 100755 index 000000000..fe9ddac3b --- /dev/null +++ b/app/src/main/res/anim/anim_none.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_bottom_in.xml b/app/src/main/res/anim/anim_readbook_bottom_in.xml new file mode 100755 index 000000000..48fe0c0aa --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_bottom_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_bottom_out.xml b/app/src/main/res/anim/anim_readbook_bottom_out.xml new file mode 100755 index 000000000..2e91beb95 --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_bottom_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_top_in.xml b/app/src/main/res/anim/anim_readbook_top_in.xml new file mode 100755 index 000000000..e36360bc0 --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_top_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/anim_readbook_top_out.xml b/app/src/main/res/anim/anim_readbook_top_out.xml new file mode 100755 index 000000000..0b8acbceb --- /dev/null +++ b/app/src/main/res/anim/anim_readbook_top_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_image.xml b/app/src/main/res/color/selector_image.xml new file mode 100755 index 000000000..ffc800f9f --- /dev/null +++ b/app/src/main/res/color/selector_image.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_chapter_item_divider.xml b/app/src/main/res/drawable/bg_chapter_item_divider.xml new file mode 100755 index 000000000..38db10eaf --- /dev/null +++ b/app/src/main/res/drawable/bg_chapter_item_divider.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_edit.xml b/app/src/main/res/drawable/bg_edit.xml new file mode 100755 index 000000000..9653cca5e --- /dev/null +++ b/app/src/main/res/drawable/bg_edit.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_eink_border_bottom.xml b/app/src/main/res/drawable/bg_eink_border_bottom.xml new file mode 100755 index 000000000..3725bc1a7 --- /dev/null +++ b/app/src/main/res/drawable/bg_eink_border_bottom.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_eink_border_dialog.xml b/app/src/main/res/drawable/bg_eink_border_dialog.xml new file mode 100755 index 000000000..d49d3ae34 --- /dev/null +++ b/app/src/main/res/drawable/bg_eink_border_dialog.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_eink_border_top.xml b/app/src/main/res/drawable/bg_eink_border_top.xml new file mode 100755 index 000000000..137419f36 --- /dev/null +++ b/app/src/main/res/drawable/bg_eink_border_top.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_find_book_group.xml b/app/src/main/res/drawable/bg_find_book_group.xml new file mode 100755 index 000000000..b00a89b22 --- /dev/null +++ b/app/src/main/res/drawable/bg_find_book_group.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_gradient.xml b/app/src/main/res/drawable/bg_gradient.xml new file mode 100755 index 000000000..6c2f91672 --- /dev/null +++ b/app/src/main/res/drawable/bg_gradient.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_img_border.xml b/app/src/main/res/drawable/bg_img_border.xml new file mode 100755 index 000000000..07783726c --- /dev/null +++ b/app/src/main/res/drawable/bg_img_border.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_item_focused_on_tv.xml b/app/src/main/res/drawable/bg_item_focused_on_tv.xml new file mode 100755 index 000000000..5345404f6 --- /dev/null +++ b/app/src/main/res/drawable/bg_item_focused_on_tv.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_popup_menu.xml b/app/src/main/res/drawable/bg_popup_menu.xml new file mode 100755 index 000000000..b51849bcd --- /dev/null +++ b/app/src/main/res/drawable/bg_popup_menu.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_prefs_color.xml b/app/src/main/res/drawable/bg_prefs_color.xml new file mode 100755 index 000000000..6fce82fd7 --- /dev/null +++ b/app/src/main/res/drawable/bg_prefs_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_searchview.xml b/app/src/main/res/drawable/bg_searchview.xml new file mode 100755 index 000000000..beee4af1e --- /dev/null +++ b/app/src/main/res/drawable/bg_searchview.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_bottom.xml b/app/src/main/res/drawable/bg_shadow_bottom.xml new file mode 100755 index 000000000..68063fd27 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_bottom.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_bottom_night.xml b/app/src/main/res/drawable/bg_shadow_bottom_night.xml new file mode 100755 index 000000000..039ec1696 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_bottom_night.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_top.xml b/app/src/main/res/drawable/bg_shadow_top.xml new file mode 100755 index 000000000..98f1df214 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_top.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shadow_top_night.xml b/app/src/main/res/drawable/bg_shadow_top_night.xml new file mode 100755 index 000000000..2ef90a767 --- /dev/null +++ b/app/src/main/res/drawable/bg_shadow_top_night.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_textfield_search.xml b/app/src/main/res/drawable/bg_textfield_search.xml new file mode 100755 index 000000000..8069a07d8 --- /dev/null +++ b/app/src/main/res/drawable/bg_textfield_search.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fastscroll_bubble.xml b/app/src/main/res/drawable/fastscroll_bubble.xml new file mode 100755 index 000000000..f72b616ae --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_bubble.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fastscroll_handle.xml b/app/src/main/res/drawable/fastscroll_handle.xml new file mode 100755 index 000000000..8671a01cf --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_handle.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fastscroll_track.xml b/app/src/main/res/drawable/fastscroll_track.xml new file mode 100755 index 000000000..854768787 --- /dev/null +++ b/app/src/main/res/drawable/fastscroll_track.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100755 index 000000000..c7542ba87 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_online.xml b/app/src/main/res/drawable/ic_add_online.xml new file mode 100755 index 000000000..8a3cc9b8b --- /dev/null +++ b/app/src/main/res/drawable/ic_add_online.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrange.xml b/app/src/main/res/drawable/ic_arrange.xml new file mode 100755 index 000000000..ce2d50152 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrange.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100755 index 000000000..2d68f797b --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100755 index 000000000..8fca50752 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_down.xml b/app/src/main/res/drawable/ic_arrow_drop_down.xml new file mode 100755 index 000000000..df5ff10d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_down.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_up.xml b/app/src/main/res/drawable/ic_arrow_drop_up.xml new file mode 100755 index 000000000..ed31816be --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100755 index 000000000..12d0efb40 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_author.xml b/app/src/main/res/drawable/ic_author.xml new file mode 100755 index 000000000..00d53f351 --- /dev/null +++ b/app/src/main/res/drawable/ic_author.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_auto_page.xml b/app/src/main/res/drawable/ic_auto_page.xml new file mode 100755 index 000000000..bec2a48f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_auto_page.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_auto_page_stop.xml b/app/src/main/res/drawable/ic_auto_page_stop.xml new file mode 100755 index 000000000..6a6c74535 --- /dev/null +++ b/app/src/main/res/drawable/ic_auto_page_stop.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_backup.xml b/app/src/main/res/drawable/ic_backup.xml new file mode 100755 index 000000000..200bb7081 --- /dev/null +++ b/app/src/main/res/drawable/ic_backup.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_close.xml b/app/src/main/res/drawable/ic_baseline_close.xml new file mode 100755 index 000000000..b8af066c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_sort_24.xml b/app/src/main/res/drawable/ic_baseline_sort_24.xml new file mode 100755 index 000000000..3b184191d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_sort_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_book_has.xml b/app/src/main/res/drawable/ic_book_has.xml new file mode 100755 index 000000000..a920e50ae --- /dev/null +++ b/app/src/main/res/drawable/ic_book_has.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_book_last.xml b/app/src/main/res/drawable/ic_book_last.xml new file mode 100755 index 000000000..82b0842d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_book_last.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bookmark.xml b/app/src/main/res/drawable/ic_bookmark.xml new file mode 100755 index 000000000..551bee2c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_books.xml b/app/src/main/res/drawable/ic_bottom_books.xml new file mode 100755 index 000000000..807aaaf30 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_books.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_books_e.xml b/app/src/main/res/drawable/ic_bottom_books_e.xml new file mode 100755 index 000000000..04d36c375 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_books_e.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_books_s.xml b/app/src/main/res/drawable/ic_bottom_books_s.xml new file mode 100755 index 000000000..8f5f72542 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_books_s.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_explore.xml b/app/src/main/res/drawable/ic_bottom_explore.xml new file mode 100755 index 000000000..439f69ba9 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_explore.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_explore_e.xml b/app/src/main/res/drawable/ic_bottom_explore_e.xml new file mode 100755 index 000000000..47e4bde9d --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_explore_e.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_explore_s.xml b/app/src/main/res/drawable/ic_bottom_explore_s.xml new file mode 100755 index 000000000..6dc6e4322 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_explore_s.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bottom_person.xml b/app/src/main/res/drawable/ic_bottom_person.xml new file mode 100755 index 000000000..50053997a --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_person.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_person_e.xml b/app/src/main/res/drawable/ic_bottom_person_e.xml new file mode 100755 index 000000000..d01128391 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_person_e.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_person_s.xml b/app/src/main/res/drawable/ic_bottom_person_s.xml new file mode 100755 index 000000000..3ff7fc8c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_person_s.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_rss_feed.xml b/app/src/main/res/drawable/ic_bottom_rss_feed.xml new file mode 100755 index 000000000..4cd080b7f --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_rss_feed.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_rss_feed_e.xml b/app/src/main/res/drawable/ic_bottom_rss_feed_e.xml new file mode 100755 index 000000000..c1c4ba0e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_rss_feed_e.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bottom_rss_feed_s.xml b/app/src/main/res/drawable/ic_bottom_rss_feed_s.xml new file mode 100755 index 000000000..a73c01430 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_rss_feed_s.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_brightness.xml b/app/src/main/res/drawable/ic_brightness.xml new file mode 100755 index 000000000..ac37ba7d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_brightness_auto.xml b/app/src/main/res/drawable/ic_brightness_auto.xml new file mode 100755 index 000000000..6ea7b953e --- /dev/null +++ b/app/src/main/res/drawable/ic_brightness_auto.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bubble_chart.xml b/app/src/main/res/drawable/ic_bubble_chart.xml new file mode 100755 index 000000000..8914d76e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_bubble_chart.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_bug_report.xml b/app/src/main/res/drawable/ic_bug_report.xml new file mode 100755 index 000000000..ac83c8c90 --- /dev/null +++ b/app/src/main/res/drawable/ic_bug_report.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cfg_about.xml b/app/src/main/res/drawable/ic_cfg_about.xml new file mode 100755 index 000000000..32aa0c4e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_about.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_cfg_backup.xml b/app/src/main/res/drawable/ic_cfg_backup.xml new file mode 100755 index 000000000..548722c75 --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_backup.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cfg_donate.xml b/app/src/main/res/drawable/ic_cfg_donate.xml new file mode 100755 index 000000000..9d5bb4788 --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_donate.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cfg_other.xml b/app/src/main/res/drawable/ic_cfg_other.xml new file mode 100755 index 000000000..e9961a4ab --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_other.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cfg_replace.xml b/app/src/main/res/drawable/ic_cfg_replace.xml new file mode 100755 index 000000000..6b27b5a5a --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_replace.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cfg_source.xml b/app/src/main/res/drawable/ic_cfg_source.xml new file mode 100755 index 000000000..41044ee7c --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_source.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cfg_theme.xml b/app/src/main/res/drawable/ic_cfg_theme.xml new file mode 100755 index 000000000..fb59a6a7a --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_theme.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_cfg_web.xml b/app/src/main/res/drawable/ic_cfg_web.xml new file mode 100755 index 000000000..7c3b9cac1 --- /dev/null +++ b/app/src/main/res/drawable/ic_cfg_web.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chapter_list.xml b/app/src/main/res/drawable/ic_chapter_list.xml new file mode 100755 index 000000000..d64d58dbf --- /dev/null +++ b/app/src/main/res/drawable/ic_chapter_list.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100755 index 000000000..620bc9133 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_source.xml b/app/src/main/res/drawable/ic_check_source.xml new file mode 100755 index 000000000..cbd10fd70 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_source.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml new file mode 100755 index 000000000..dfa860c00 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_all.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100755 index 000000000..cdf136c9c --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_create_folder_outline.xml b/app/src/main/res/drawable/ic_create_folder_outline.xml new file mode 100755 index 000000000..8428fd381 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_folder_outline.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cursor_left.xml b/app/src/main/res/drawable/ic_cursor_left.xml new file mode 100755 index 000000000..1656763c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_cursor_left.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cursor_right.xml b/app/src/main/res/drawable/ic_cursor_right.xml new file mode 100755 index 000000000..99734ea19 --- /dev/null +++ b/app/src/main/res/drawable/ic_cursor_right.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_daytime.xml b/app/src/main/res/drawable/ic_daytime.xml new file mode 100755 index 000000000..60921c1dc --- /dev/null +++ b/app/src/main/res/drawable/ic_daytime.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_divider.xml b/app/src/main/res/drawable/ic_divider.xml new file mode 100755 index 000000000..21a88f21a --- /dev/null +++ b/app/src/main/res/drawable/ic_divider.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100755 index 000000000..bf296eceb --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_download_line.xml b/app/src/main/res/drawable/ic_download_line.xml new file mode 100755 index 000000000..25dbe36c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_line.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100755 index 000000000..2d249dde4 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exchange.xml b/app/src/main/res/drawable/ic_exchange.xml new file mode 100755 index 000000000..00fa9cc95 --- /dev/null +++ b/app/src/main/res/drawable/ic_exchange.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exchange_order.xml b/app/src/main/res/drawable/ic_exchange_order.xml new file mode 100755 index 000000000..2beeb7441 --- /dev/null +++ b/app/src/main/res/drawable/ic_exchange_order.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_exit.xml b/app/src/main/res/drawable/ic_exit.xml new file mode 100755 index 000000000..c16013837 --- /dev/null +++ b/app/src/main/res/drawable/ic_exit.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_expand_less.xml b/app/src/main/res/drawable/ic_expand_less.xml new file mode 100755 index 000000000..f70feed92 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_less.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more.xml b/app/src/main/res/drawable/ic_expand_more.xml new file mode 100755 index 000000000..5d10f3768 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_export.xml b/app/src/main/res/drawable/ic_export.xml new file mode 100755 index 000000000..2c34ad3b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_export.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_fast_forward.xml b/app/src/main/res/drawable/ic_fast_forward.xml new file mode 100755 index 000000000..13fcad8cf --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fast_rewind.xml b/app/src/main/res/drawable/ic_fast_rewind.xml new file mode 100755 index 000000000..ea721ad89 --- /dev/null +++ b/app/src/main/res/drawable/ic_fast_rewind.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_find_replace.xml b/app/src/main/res/drawable/ic_find_replace.xml new file mode 100755 index 000000000..07bbfc39c --- /dev/null +++ b/app/src/main/res/drawable/ic_find_replace.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml new file mode 100755 index 000000000..cb2a84ca4 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_open.xml b/app/src/main/res/drawable/ic_folder_open.xml new file mode 100755 index 000000000..4235565f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_open.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_outline.xml b/app/src/main/res/drawable/ic_folder_outline.xml new file mode 100755 index 000000000..f034f89ae --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_outline.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_groups.xml b/app/src/main/res/drawable/ic_groups.xml new file mode 100755 index 000000000..0ebcd4526 --- /dev/null +++ b/app/src/main/res/drawable/ic_groups.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100755 index 000000000..1cce9b796 --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100755 index 000000000..095e0f7ab --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_image.xml b/app/src/main/res/drawable/ic_image.xml new file mode 100755 index 000000000..bfd8e5c65 --- /dev/null +++ b/app/src/main/res/drawable/ic_image.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml new file mode 100755 index 000000000..dc493329e --- /dev/null +++ b/app/src/main/res/drawable/ic_import.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_interface_setting.xml b/app/src/main/res/drawable/ic_interface_setting.xml new file mode 100755 index 000000000..139b00724 --- /dev/null +++ b/app/src/main/res/drawable/ic_interface_setting.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher1.xml b/app/src/main/res/drawable/ic_launcher1.xml new file mode 100755 index 000000000..18c829634 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher1.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher1_b.xml b/app/src/main/res/drawable/ic_launcher1_b.xml new file mode 100755 index 000000000..b3609b979 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher1_b.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher2.xml b/app/src/main/res/drawable/ic_launcher2.xml new file mode 100755 index 000000000..bead863ac --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher2.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher3.xml b/app/src/main/res/drawable/ic_launcher3.xml new file mode 100755 index 000000000..a79648990 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher3.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher4.xml b/app/src/main/res/drawable/ic_launcher4.xml new file mode 100755 index 000000000..23e81a233 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher4.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher4_b.xml b/app/src/main/res/drawable/ic_launcher4_b.xml new file mode 100755 index 000000000..3417dbbce --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher4_b.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher5.xml b/app/src/main/res/drawable/ic_launcher5.xml new file mode 100755 index 000000000..8b24265f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher5.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher5_b.xml b/app/src/main/res/drawable/ic_launcher5_b.xml new file mode 100755 index 000000000..f2e02dbf2 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher5_b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher6.xml b/app/src/main/res/drawable/ic_launcher6.xml new file mode 100755 index 000000000..1bb711f8f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher6.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher7.xml b/app/src/main/res/drawable/ic_launcher7.xml new file mode 100755 index 000000000..5649650e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher7.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher7_b.xml b/app/src/main/res/drawable/ic_launcher7_b.xml new file mode 100755 index 000000000..07609b3c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher7_b.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock_outline.xml b/app/src/main/res/drawable/ic_lock_outline.xml new file mode 100755 index 000000000..f9edaf4d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_outline.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100755 index 000000000..8a6004bd4 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100755 index 000000000..091114e15 --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml new file mode 100755 index 000000000..7b7f19554 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_network_check.xml b/app/src/main/res/drawable/ic_network_check.xml new file mode 100755 index 000000000..9d88ef1ae --- /dev/null +++ b/app/src/main/res/drawable/ic_network_check.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_cloud_24.xml b/app/src/main/res/drawable/ic_outline_cloud_24.xml new file mode 100755 index 000000000..c8b092ead --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_cloud_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_delete.xml b/app/src/main/res/drawable/ic_outline_delete.xml new file mode 100755 index 000000000..734188c36 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_24dp.xml b/app/src/main/res/drawable/ic_pause_24dp.xml new file mode 100755 index 000000000..193030b12 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause_outline_24dp.xml b/app/src/main/res/drawable/ic_pause_outline_24dp.xml new file mode 100755 index 000000000..11f30c38d --- /dev/null +++ b/app/src/main/res/drawable/ic_pause_outline_24dp.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_24dp.xml b/app/src/main/res/drawable/ic_play_24dp.xml new file mode 100755 index 000000000..4250d72cb --- /dev/null +++ b/app/src/main/res/drawable/ic_play_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_mode_list_end_stop.xml b/app/src/main/res/drawable/ic_play_mode_list_end_stop.xml new file mode 100755 index 000000000..edc09be4e --- /dev/null +++ b/app/src/main/res/drawable/ic_play_mode_list_end_stop.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play_mode_list_loop.xml b/app/src/main/res/drawable/ic_play_mode_list_loop.xml new file mode 100755 index 000000000..9137c0298 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_mode_list_loop.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_mode_random.xml b/app/src/main/res/drawable/ic_play_mode_random.xml new file mode 100755 index 000000000..159b056a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_mode_random.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_mode_single_loop.xml b/app/src/main/res/drawable/ic_play_mode_single_loop.xml new file mode 100755 index 000000000..27014b26e --- /dev/null +++ b/app/src/main/res/drawable/ic_play_mode_single_loop.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_outline_24dp.xml b/app/src/main/res/drawable/ic_play_outline_24dp.xml new file mode 100755 index 000000000..deb25e9f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_outline_24dp.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_praise.xml b/app/src/main/res/drawable/ic_praise.xml new file mode 100755 index 000000000..a1ccb8566 --- /dev/null +++ b/app/src/main/res/drawable/ic_praise.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_read_aloud.xml b/app/src/main/res/drawable/ic_read_aloud.xml new file mode 100755 index 000000000..7978d2196 --- /dev/null +++ b/app/src/main/res/drawable/ic_read_aloud.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_reduce.xml b/app/src/main/res/drawable/ic_reduce.xml new file mode 100755 index 000000000..90eec6f30 --- /dev/null +++ b/app/src/main/res/drawable/ic_reduce.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_black_24dp.xml b/app/src/main/res/drawable/ic_refresh_black_24dp.xml new file mode 100755 index 000000000..fc7f629d8 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_black_24dp.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml new file mode 100755 index 000000000..9e4f8dfcc --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml new file mode 100755 index 000000000..d9f75ea6d --- /dev/null +++ b/app/src/main/res/drawable/ic_restore.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml new file mode 100755 index 000000000..042962ee5 --- /dev/null +++ b/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_scan.xml b/app/src/main/res/drawable/ic_scan.xml new file mode 100755 index 000000000..2ca607005 --- /dev/null +++ b/app/src/main/res/drawable/ic_scan.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_scoring.xml b/app/src/main/res/drawable/ic_scoring.xml new file mode 100755 index 000000000..683b68107 --- /dev/null +++ b/app/src/main/res/drawable/ic_scoring.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_screen.xml b/app/src/main/res/drawable/ic_screen.xml new file mode 100755 index 000000000..4a641e517 --- /dev/null +++ b/app/src/main/res/drawable/ic_screen.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100755 index 000000000..921e00d7a --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_search_hint.xml b/app/src/main/res/drawable/ic_search_hint.xml new file mode 100755 index 000000000..42de64c26 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_hint.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100755 index 000000000..1d4297767 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100755 index 000000000..df884e817 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml new file mode 100755 index 000000000..eae4da1bf --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_previous.xml b/app/src/main/res/drawable/ic_skip_previous.xml new file mode 100755 index 000000000..abb9944fc --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_previous.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100755 index 000000000..264057420 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100755 index 000000000..a2ebf3532 --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border.xml b/app/src/main/res/drawable/ic_star_border.xml new file mode 100755 index 000000000..c452e47ec --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stop_black_24dp.xml b/app/src/main/res/drawable/ic_stop_black_24dp.xml new file mode 100755 index 000000000..025a8b82e --- /dev/null +++ b/app/src/main/res/drawable/ic_stop_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_storage_black_24dp.xml b/app/src/main/res/drawable/ic_storage_black_24dp.xml new file mode 100755 index 000000000..b11623929 --- /dev/null +++ b/app/src/main/res/drawable/ic_storage_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_swap_horiz.xml b/app/src/main/res/drawable/ic_swap_horiz.xml new file mode 100755 index 000000000..8d80d0b55 --- /dev/null +++ b/app/src/main/res/drawable/ic_swap_horiz.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_time_add_24dp.xml b/app/src/main/res/drawable/ic_time_add_24dp.xml new file mode 100755 index 000000000..66afd8f8f --- /dev/null +++ b/app/src/main/res/drawable/ic_time_add_24dp.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_timer_black_24dp.xml b/app/src/main/res/drawable/ic_timer_black_24dp.xml new file mode 100755 index 000000000..df5ad4e8a --- /dev/null +++ b/app/src/main/res/drawable/ic_timer_black_24dp.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_toc.xml b/app/src/main/res/drawable/ic_toc.xml new file mode 100755 index 000000000..c64161dc1 --- /dev/null +++ b/app/src/main/res/drawable/ic_toc.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml new file mode 100755 index 000000000..ae2a4dde7 --- /dev/null +++ b/app/src/main/res/drawable/ic_translate.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100755 index 000000000..2490a577d --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_view_quilt.xml b/app/src/main/res/drawable/ic_view_quilt.xml new file mode 100755 index 000000000..a0fb3d300 --- /dev/null +++ b/app/src/main/res/drawable/ic_view_quilt.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml new file mode 100755 index 000000000..c71061582 --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up.xml b/app/src/main/res/drawable/ic_volume_up.xml new file mode 100755 index 000000000..5d604f823 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_web_outline.xml b/app/src/main/res/drawable/ic_web_outline.xml new file mode 100755 index 000000000..adaeb5644 --- /dev/null +++ b/app/src/main/res/drawable/ic_web_outline.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_web_service_noti.xml b/app/src/main/res/drawable/ic_web_service_noti.xml new file mode 100755 index 000000000..ab93b7528 --- /dev/null +++ b/app/src/main/res/drawable/ic_web_service_noti.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/icon_read_book.png b/app/src/main/res/drawable/icon_read_book.png new file mode 100755 index 0000000000000000000000000000000000000000..81b4025a3fcdbd73558f91a6915003ee650d9324 GIT binary patch literal 2716 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/image_legado.png b/app/src/main/res/drawable/image_legado.png new file mode 100755 index 0000000000000000000000000000000000000000..20f44239e54f878ee3194baf0112ee76376a4e26 GIT binary patch literal 7656 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¬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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/image_rss.jpg b/app/src/main/res/drawable/image_rss.jpg new file mode 100755 index 0000000000000000000000000000000000000000..884fe219bb67f8c07ad6a47e7cdecd3069e5366d GIT binary patch literal 8595 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 DOguzQ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/image_rss_article.jpg b/app/src/main/res/drawable/image_rss_article.jpg new file mode 100755 index 0000000000000000000000000000000000000000..74e9ec86dac96df7d51c0f5cdb35a7e1f42af240 GIT binary patch literal 13793 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 Bj4l8G literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/recyclerview_divider_horizontal.xml b/app/src/main/res/drawable/recyclerview_divider_horizontal.xml new file mode 100755 index 000000000..2d2d95362 --- /dev/null +++ b/app/src/main/res/drawable/recyclerview_divider_horizontal.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recyclerview_divider_vertical.xml b/app/src/main/res/drawable/recyclerview_divider_vertical.xml new file mode 100755 index 000000000..cf8d2882a --- /dev/null +++ b/app/src/main/res/drawable/recyclerview_divider_vertical.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_btn_accent_bg.xml b/app/src/main/res/drawable/selector_btn_accent_bg.xml new file mode 100755 index 000000000..d06cfe139 --- /dev/null +++ b/app/src/main/res/drawable/selector_btn_accent_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_circle_btn_bg.xml b/app/src/main/res/drawable/selector_circle_btn_bg.xml new file mode 100755 index 000000000..6144b889b --- /dev/null +++ b/app/src/main/res/drawable/selector_circle_btn_bg.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_common_bg.xml b/app/src/main/res/drawable/selector_common_bg.xml new file mode 100755 index 000000000..2174a7333 --- /dev/null +++ b/app/src/main/res/drawable/selector_common_bg.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_fillet_btn_bg.xml b/app/src/main/res/drawable/selector_fillet_btn_bg.xml new file mode 100755 index 000000000..431a579d3 --- /dev/null +++ b/app/src/main/res/drawable/selector_fillet_btn_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_tv_black.xml b/app/src/main/res/drawable/selector_tv_black.xml new file mode 100755 index 000000000..fbbeed245 --- /dev/null +++ b/app/src/main/res/drawable/selector_tv_black.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_card_view.xml b/app/src/main/res/drawable/shape_card_view.xml new file mode 100755 index 000000000..d49a2d5c1 --- /dev/null +++ b/app/src/main/res/drawable/shape_card_view.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle.xml b/app/src/main/res/drawable/shape_circle.xml new file mode 100755 index 000000000..a46383db2 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fillet_btn.xml b/app/src/main/res/drawable/shape_fillet_btn.xml new file mode 100755 index 000000000..83ccf2776 --- /dev/null +++ b/app/src/main/res/drawable/shape_fillet_btn.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_fillet_btn_press.xml b/app/src/main/res/drawable/shape_fillet_btn_press.xml new file mode 100755 index 000000000..b0c46f6f5 --- /dev/null +++ b/app/src/main/res/drawable/shape_fillet_btn_press.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_pop_checkaddshelf_bg.xml b/app/src/main/res/drawable/shape_pop_checkaddshelf_bg.xml new file mode 100755 index 000000000..215f52826 --- /dev/null +++ b/app/src/main/res/drawable/shape_pop_checkaddshelf_bg.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_radius_10dp.xml b/app/src/main/res/drawable/shape_radius_10dp.xml new file mode 100755 index 000000000..3680c9e7e --- /dev/null +++ b/app/src/main/res/drawable/shape_radius_10dp.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_radius_1dp.xml b/app/src/main/res/drawable/shape_radius_1dp.xml new file mode 100755 index 000000000..d8e0c3f87 --- /dev/null +++ b/app/src/main/res/drawable/shape_radius_1dp.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_space_divider.xml b/app/src/main/res/drawable/shape_space_divider.xml new file mode 100755 index 000000000..954360298 --- /dev/null +++ b/app/src/main/res/drawable/shape_space_divider.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_text_cursor.xml b/app/src/main/res/drawable/shape_text_cursor.xml new file mode 100755 index 000000000..71871e0e0 --- /dev/null +++ b/app/src/main/res/drawable/shape_text_cursor.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_translucent_card.xml b/app/src/main/res/drawable/shape_translucent_card.xml new file mode 100755 index 000000000..827bc335d --- /dev/null +++ b/app/src/main/res/drawable/shape_translucent_card.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_book_info.xml b/app/src/main/res/layout-land/activity_book_info.xml new file mode 100755 index 000000000..b3bd0265e --- /dev/null +++ b/app/src/main/res/layout-land/activity_book_info.xml @@ -0,0 +1,423 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100755 index 000000000..670b2b221 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_all_bookmark.xml b/app/src/main/res/layout/activity_all_bookmark.xml new file mode 100755 index 000000000..e380f3fd3 --- /dev/null +++ b/app/src/main/res/layout/activity_all_bookmark.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_arrange_book.xml b/app/src/main/res/layout/activity_arrange_book.xml new file mode 100755 index 000000000..178ff06fe --- /dev/null +++ b/app/src/main/res/layout/activity_arrange_book.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_audio_play.xml b/app/src/main/res/layout/activity_audio_play.xml new file mode 100755 index 000000000..9ec0976d0 --- /dev/null +++ b/app/src/main/res/layout/activity_audio_play.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_book_info.xml b/app/src/main/res/layout/activity_book_info.xml new file mode 100755 index 000000000..2515362a1 --- /dev/null +++ b/app/src/main/res/layout/activity_book_info.xml @@ -0,0 +1,422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_book_info_edit.xml b/app/src/main/res/layout/activity_book_info_edit.xml new file mode 100755 index 000000000..0fb0879ec --- /dev/null +++ b/app/src/main/res/layout/activity_book_info_edit.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_read.xml b/app/src/main/res/layout/activity_book_read.xml new file mode 100755 index 000000000..703c0ef5e --- /dev/null +++ b/app/src/main/res/layout/activity_book_read.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_search.xml b/app/src/main/res/layout/activity_book_search.xml new file mode 100755 index 000000000..cff9bc3ed --- /dev/null +++ b/app/src/main/res/layout/activity_book_search.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_book_source.xml b/app/src/main/res/layout/activity_book_source.xml new file mode 100755 index 000000000..21f662944 --- /dev/null +++ b/app/src/main/res/layout/activity_book_source.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_source_edit.xml b/app/src/main/res/layout/activity_book_source_edit.xml new file mode 100755 index 000000000..f75098dfc --- /dev/null +++ b/app/src/main/res/layout/activity_book_source_edit.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_cache_book.xml b/app/src/main/res/layout/activity_cache_book.xml new file mode 100755 index 000000000..0fbb63c97 --- /dev/null +++ b/app/src/main/res/layout/activity_cache_book.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chapter_list.xml b/app/src/main/res/layout/activity_chapter_list.xml new file mode 100755 index 000000000..9bceaa108 --- /dev/null +++ b/app/src/main/res/layout/activity_chapter_list.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_config.xml b/app/src/main/res/layout/activity_config.xml new file mode 100755 index 000000000..f0312ee9c --- /dev/null +++ b/app/src/main/res/layout/activity_config.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dict_rule.xml b/app/src/main/res/layout/activity_dict_rule.xml new file mode 100755 index 000000000..afd5bac58 --- /dev/null +++ b/app/src/main/res/layout/activity_dict_rule.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_donate.xml b/app/src/main/res/layout/activity_donate.xml new file mode 100755 index 000000000..522b6af92 --- /dev/null +++ b/app/src/main/res/layout/activity_donate.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_explore_show.xml b/app/src/main/res/layout/activity_explore_show.xml new file mode 100755 index 000000000..35ab2667a --- /dev/null +++ b/app/src/main/res/layout/activity_explore_show.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_file_manage.xml b/app/src/main/res/layout/activity_file_manage.xml new file mode 100755 index 000000000..73e56a2de --- /dev/null +++ b/app/src/main/res/layout/activity_file_manage.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_import_book.xml b/app/src/main/res/layout/activity_import_book.xml new file mode 100755 index 000000000..3ec0e2f0f --- /dev/null +++ b/app/src/main/res/layout/activity_import_book.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100755 index 000000000..c73e61483 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_manga.xml b/app/src/main/res/layout/activity_manga.xml new file mode 100755 index 000000000..d7c899bc2 --- /dev/null +++ b/app/src/main/res/layout/activity_manga.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_qrcode_capture.xml b/app/src/main/res/layout/activity_qrcode_capture.xml new file mode 100755 index 000000000..379109187 --- /dev/null +++ b/app/src/main/res/layout/activity_qrcode_capture.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_read_record.xml b/app/src/main/res/layout/activity_read_record.xml new file mode 100755 index 000000000..ce0f66745 --- /dev/null +++ b/app/src/main/res/layout/activity_read_record.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_replace_edit.xml b/app/src/main/res/layout/activity_replace_edit.xml new file mode 100755 index 000000000..5e136534c --- /dev/null +++ b/app/src/main/res/layout/activity_replace_edit.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_replace_rule.xml b/app/src/main/res/layout/activity_replace_rule.xml new file mode 100755 index 000000000..eeb2a26f9 --- /dev/null +++ b/app/src/main/res/layout/activity_replace_rule.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_rss_artivles.xml b/app/src/main/res/layout/activity_rss_artivles.xml new file mode 100755 index 000000000..27a0dff9d --- /dev/null +++ b/app/src/main/res/layout/activity_rss_artivles.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_favorites.xml b/app/src/main/res/layout/activity_rss_favorites.xml new file mode 100755 index 000000000..59e3ab0fa --- /dev/null +++ b/app/src/main/res/layout/activity_rss_favorites.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_read.xml b/app/src/main/res/layout/activity_rss_read.xml new file mode 100755 index 000000000..4ac626b50 --- /dev/null +++ b/app/src/main/res/layout/activity_rss_read.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_source.xml b/app/src/main/res/layout/activity_rss_source.xml new file mode 100755 index 000000000..703eea64f --- /dev/null +++ b/app/src/main/res/layout/activity_rss_source.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_source_edit.xml b/app/src/main/res/layout/activity_rss_source_edit.xml new file mode 100755 index 000000000..eecc0f915 --- /dev/null +++ b/app/src/main/res/layout/activity_rss_source_edit.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rule_sub.xml b/app/src/main/res/layout/activity_rule_sub.xml new file mode 100755 index 000000000..bf76be8fa --- /dev/null +++ b/app/src/main/res/layout/activity_rule_sub.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search_content.xml b/app/src/main/res/layout/activity_search_content.xml new file mode 100755 index 000000000..2eb097369 --- /dev/null +++ b/app/src/main/res/layout/activity_search_content.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_source_debug.xml b/app/src/main/res/layout/activity_source_debug.xml new file mode 100755 index 000000000..0d2970f2b --- /dev/null +++ b/app/src/main/res/layout/activity_source_debug.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_source_login.xml b/app/src/main/res/layout/activity_source_login.xml new file mode 100755 index 000000000..ea051953b --- /dev/null +++ b/app/src/main/res/layout/activity_source_login.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_translucence.xml b/app/src/main/res/layout/activity_translucence.xml new file mode 100755 index 000000000..143f44d2a --- /dev/null +++ b/app/src/main/res/layout/activity_translucence.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_txt_toc_rule.xml b/app/src/main/res/layout/activity_txt_toc_rule.xml new file mode 100755 index 000000000..d587b864d --- /dev/null +++ b/app/src/main/res/layout/activity_txt_toc_rule.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_web_view.xml b/app/src/main/res/layout/activity_web_view.xml new file mode 100755 index 000000000..4ac626b50 --- /dev/null +++ b/app/src/main/res/layout/activity_web_view.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml new file mode 100755 index 000000000..0d21e9842 --- /dev/null +++ b/app/src/main/res/layout/activity_welcome.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_add_to_bookshelf.xml b/app/src/main/res/layout/dialog_add_to_bookshelf.xml new file mode 100755 index 000000000..9fb1a3511 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_to_bookshelf.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_auto_read.xml b/app/src/main/res/layout/dialog_auto_read.xml new file mode 100755 index 000000000..7d56022bf --- /dev/null +++ b/app/src/main/res/layout/dialog_auto_read.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_book_change_source.xml b/app/src/main/res/layout/dialog_book_change_source.xml new file mode 100755 index 000000000..af32da053 --- /dev/null +++ b/app/src/main/res/layout/dialog_book_change_source.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_book_group_edit.xml b/app/src/main/res/layout/dialog_book_group_edit.xml new file mode 100755 index 000000000..071f87724 --- /dev/null +++ b/app/src/main/res/layout/dialog_book_group_edit.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_book_group_picker.xml b/app/src/main/res/layout/dialog_book_group_picker.xml new file mode 100755 index 000000000..95317f693 --- /dev/null +++ b/app/src/main/res/layout/dialog_book_group_picker.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_bookmark.xml b/app/src/main/res/layout/dialog_bookmark.xml new file mode 100755 index 000000000..b3c9f1796 --- /dev/null +++ b/app/src/main/res/layout/dialog_bookmark.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_bookshelf_config.xml b/app/src/main/res/layout/dialog_bookshelf_config.xml new file mode 100755 index 000000000..b40582749 --- /dev/null +++ b/app/src/main/res/layout/dialog_bookshelf_config.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_change_cover.xml b/app/src/main/res/layout/dialog_change_cover.xml new file mode 100755 index 000000000..2dee2288c --- /dev/null +++ b/app/src/main/res/layout/dialog_change_cover.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_chapter_change_source.xml b/app/src/main/res/layout/dialog_chapter_change_source.xml new file mode 100755 index 000000000..97cc032ba --- /dev/null +++ b/app/src/main/res/layout/dialog_chapter_change_source.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_check_source_config.xml b/app/src/main/res/layout/dialog_check_source_config.xml new file mode 100755 index 000000000..0b43f38f1 --- /dev/null +++ b/app/src/main/res/layout/dialog_check_source_config.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_click_action_config.xml b/app/src/main/res/layout/dialog_click_action_config.xml new file mode 100755 index 000000000..07ba69132 --- /dev/null +++ b/app/src/main/res/layout/dialog_click_action_config.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_code_view.xml b/app/src/main/res/layout/dialog_code_view.xml new file mode 100755 index 000000000..3f0ffd8b9 --- /dev/null +++ b/app/src/main/res/layout/dialog_code_view.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_content_edit.xml b/app/src/main/res/layout/dialog_content_edit.xml new file mode 100755 index 000000000..7dbbfa9b3 --- /dev/null +++ b/app/src/main/res/layout/dialog_content_edit.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_cover_rule_config.xml b/app/src/main/res/layout/dialog_cover_rule_config.xml new file mode 100755 index 000000000..e09153226 --- /dev/null +++ b/app/src/main/res/layout/dialog_cover_rule_config.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_custom_group.xml b/app/src/main/res/layout/dialog_custom_group.xml new file mode 100755 index 000000000..7f9933964 --- /dev/null +++ b/app/src/main/res/layout/dialog_custom_group.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_dict.xml b/app/src/main/res/layout/dialog_dict.xml new file mode 100755 index 000000000..7abd83608 --- /dev/null +++ b/app/src/main/res/layout/dialog_dict.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_dict_rule_edit.xml b/app/src/main/res/layout/dialog_dict_rule_edit.xml new file mode 100755 index 000000000..26133beee --- /dev/null +++ b/app/src/main/res/layout/dialog_dict_rule_edit.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_direct_link_upload_config.xml b/app/src/main/res/layout/dialog_direct_link_upload_config.xml new file mode 100755 index 000000000..736ad7e74 --- /dev/null +++ b/app/src/main/res/layout/dialog_direct_link_upload_config.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_download_choice.xml b/app/src/main/res/layout/dialog_download_choice.xml new file mode 100755 index 000000000..8923c90f5 --- /dev/null +++ b/app/src/main/res/layout/dialog_download_choice.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_edit_text.xml b/app/src/main/res/layout/dialog_edit_text.xml new file mode 100755 index 000000000..c0666dbe6 --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_text.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_file_chooser.xml b/app/src/main/res/layout/dialog_file_chooser.xml new file mode 100755 index 000000000..6c5f87ae3 --- /dev/null +++ b/app/src/main/res/layout/dialog_file_chooser.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_font_select.xml b/app/src/main/res/layout/dialog_font_select.xml new file mode 100755 index 000000000..1920055f7 --- /dev/null +++ b/app/src/main/res/layout/dialog_font_select.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_http_tts_edit.xml b/app/src/main/res/layout/dialog_http_tts_edit.xml new file mode 100755 index 000000000..77429784b --- /dev/null +++ b/app/src/main/res/layout/dialog_http_tts_edit.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_image_blurring.xml b/app/src/main/res/layout/dialog_image_blurring.xml new file mode 100755 index 000000000..b9d20e55e --- /dev/null +++ b/app/src/main/res/layout/dialog_image_blurring.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_login.xml b/app/src/main/res/layout/dialog_login.xml new file mode 100755 index 000000000..4cf639ace --- /dev/null +++ b/app/src/main/res/layout/dialog_login.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_manga_color_filter.xml b/app/src/main/res/layout/dialog_manga_color_filter.xml new file mode 100755 index 000000000..834d39af3 --- /dev/null +++ b/app/src/main/res/layout/dialog_manga_color_filter.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_manga_epaper.xml b/app/src/main/res/layout/dialog_manga_epaper.xml new file mode 100755 index 000000000..60aca4e13 --- /dev/null +++ b/app/src/main/res/layout/dialog_manga_epaper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_manga_footer_setting.xml b/app/src/main/res/layout/dialog_manga_footer_setting.xml new file mode 100755 index 000000000..8db84425c --- /dev/null +++ b/app/src/main/res/layout/dialog_manga_footer_setting.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_multiple_edit_text.xml b/app/src/main/res/layout/dialog_multiple_edit_text.xml new file mode 100755 index 000000000..4aa20d9ae --- /dev/null +++ b/app/src/main/res/layout/dialog_multiple_edit_text.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_number_picker.xml b/app/src/main/res/layout/dialog_number_picker.xml new file mode 100755 index 000000000..f54fd0a00 --- /dev/null +++ b/app/src/main/res/layout/dialog_number_picker.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_open_url_confirm.xml b/app/src/main/res/layout/dialog_open_url_confirm.xml new file mode 100755 index 000000000..4718ba3e8 --- /dev/null +++ b/app/src/main/res/layout/dialog_open_url_confirm.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_page_key.xml b/app/src/main/res/layout/dialog_page_key.xml new file mode 100755 index 000000000..51f5094e2 --- /dev/null +++ b/app/src/main/res/layout/dialog_page_key.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_photo_view.xml b/app/src/main/res/layout/dialog_photo_view.xml new file mode 100755 index 000000000..410ac99e5 --- /dev/null +++ b/app/src/main/res/layout/dialog_photo_view.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/layout/dialog_progressbar_view.xml b/app/src/main/res/layout/dialog_progressbar_view.xml new file mode 100755 index 000000000..fdc2d7a61 --- /dev/null +++ b/app/src/main/res/layout/dialog_progressbar_view.xml @@ -0,0 +1,36 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_aloud.xml b/app/src/main/res/layout/dialog_read_aloud.xml new file mode 100755 index 000000000..8e842c9c5 --- /dev/null +++ b/app/src/main/res/layout/dialog_read_aloud.xml @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_bg_text.xml b/app/src/main/res/layout/dialog_read_bg_text.xml new file mode 100755 index 000000000..d366b9d9d --- /dev/null +++ b/app/src/main/res/layout/dialog_read_bg_text.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_book_style.xml b/app/src/main/res/layout/dialog_read_book_style.xml new file mode 100755 index 000000000..91f66fa74 --- /dev/null +++ b/app/src/main/res/layout/dialog_read_book_style.xml @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_padding.xml b/app/src/main/res/layout/dialog_read_padding.xml new file mode 100755 index 000000000..ab08bf930 --- /dev/null +++ b/app/src/main/res/layout/dialog_read_padding.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_recycler_view.xml b/app/src/main/res/layout/dialog_recycler_view.xml new file mode 100755 index 000000000..8bccd97c4 --- /dev/null +++ b/app/src/main/res/layout/dialog_recycler_view.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rss_favorite_config.xml b/app/src/main/res/layout/dialog_rss_favorite_config.xml new file mode 100755 index 000000000..382b1f3d4 --- /dev/null +++ b/app/src/main/res/layout/dialog_rss_favorite_config.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rule_sub_edit.xml b/app/src/main/res/layout/dialog_rule_sub_edit.xml new file mode 100755 index 000000000..fa3dc4b10 --- /dev/null +++ b/app/src/main/res/layout/dialog_rule_sub_edit.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_search_scope.xml b/app/src/main/res/layout/dialog_search_scope.xml new file mode 100755 index 000000000..090714f56 --- /dev/null +++ b/app/src/main/res/layout/dialog_search_scope.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_select_section_export.xml b/app/src/main/res/layout/dialog_select_section_export.xml new file mode 100755 index 000000000..ae33d9ad3 --- /dev/null +++ b/app/src/main/res/layout/dialog_select_section_export.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_simulated_reading.xml b/app/src/main/res/layout/dialog_simulated_reading.xml new file mode 100755 index 000000000..c57c498f8 --- /dev/null +++ b/app/src/main/res/layout/dialog_simulated_reading.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_source_picker.xml b/app/src/main/res/layout/dialog_source_picker.xml new file mode 100755 index 000000000..03d2a5a00 --- /dev/null +++ b/app/src/main/res/layout/dialog_source_picker.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_text_view.xml b/app/src/main/res/layout/dialog_text_view.xml new file mode 100755 index 000000000..9145506b9 --- /dev/null +++ b/app/src/main/res/layout/dialog_text_view.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_tip_config.xml b/app/src/main/res/layout/dialog_tip_config.xml new file mode 100755 index 000000000..2764e47a3 --- /dev/null +++ b/app/src/main/res/layout/dialog_tip_config.xml @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_toc_regex.xml b/app/src/main/res/layout/dialog_toc_regex.xml new file mode 100755 index 000000000..5f7bf51c9 --- /dev/null +++ b/app/src/main/res/layout/dialog_toc_regex.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_toc_regex_edit.xml b/app/src/main/res/layout/dialog_toc_regex_edit.xml new file mode 100755 index 000000000..06187d7d8 --- /dev/null +++ b/app/src/main/res/layout/dialog_toc_regex_edit.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_update.xml b/app/src/main/res/layout/dialog_update.xml new file mode 100755 index 000000000..0c3bb6245 --- /dev/null +++ b/app/src/main/res/layout/dialog_update.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_url_option_edit.xml b/app/src/main/res/layout/dialog_url_option_edit.xml new file mode 100755 index 000000000..4dd7e33f2 --- /dev/null +++ b/app/src/main/res/layout/dialog_url_option_edit.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_variable.xml b/app/src/main/res/layout/dialog_variable.xml new file mode 100755 index 000000000..91d8dafe2 --- /dev/null +++ b/app/src/main/res/layout/dialog_variable.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_verification_code_view.xml b/app/src/main/res/layout/dialog_verification_code_view.xml new file mode 100755 index 000000000..ce6648233 --- /dev/null +++ b/app/src/main/res/layout/dialog_verification_code_view.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_wait.xml b/app/src/main/res/layout/dialog_wait.xml new file mode 100755 index 000000000..9b7e3009e --- /dev/null +++ b/app/src/main/res/layout/dialog_wait.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_webdav_server.xml b/app/src/main/res/layout/dialog_webdav_server.xml new file mode 100755 index 000000000..ab4edf876 --- /dev/null +++ b/app/src/main/res/layout/dialog_webdav_server.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookmark.xml b/app/src/main/res/layout/fragment_bookmark.xml new file mode 100755 index 000000000..87bcdde1c --- /dev/null +++ b/app/src/main/res/layout/fragment_bookmark.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_books.xml b/app/src/main/res/layout/fragment_books.xml new file mode 100755 index 000000000..e17632442 --- /dev/null +++ b/app/src/main/res/layout/fragment_books.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_bookshelf1.xml b/app/src/main/res/layout/fragment_bookshelf1.xml new file mode 100755 index 000000000..91b638e02 --- /dev/null +++ b/app/src/main/res/layout/fragment_bookshelf1.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_bookshelf2.xml b/app/src/main/res/layout/fragment_bookshelf2.xml new file mode 100755 index 000000000..35ce510ec --- /dev/null +++ b/app/src/main/res/layout/fragment_bookshelf2.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chapter_list.xml b/app/src/main/res/layout/fragment_chapter_list.xml new file mode 100755 index 000000000..1d9dd8b68 --- /dev/null +++ b/app/src/main/res/layout/fragment_chapter_list.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml new file mode 100755 index 000000000..1b473573e --- /dev/null +++ b/app/src/main/res/layout/fragment_explore.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_my_config.xml b/app/src/main/res/layout/fragment_my_config.xml new file mode 100755 index 000000000..6cb3cdf4c --- /dev/null +++ b/app/src/main/res/layout/fragment_my_config.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_rss.xml b/app/src/main/res/layout/fragment_rss.xml new file mode 100755 index 000000000..f5a66f4c7 --- /dev/null +++ b/app/src/main/res/layout/fragment_rss.xml @@ -0,0 +1,42 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_rss_articles.xml b/app/src/main/res/layout/fragment_rss_articles.xml new file mode 100755 index 000000000..39ca9bca3 --- /dev/null +++ b/app/src/main/res/layout/fragment_rss_articles.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_web_view_login.xml b/app/src/main/res/layout/fragment_web_view_login.xml new file mode 100755 index 000000000..6db638382 --- /dev/null +++ b/app/src/main/res/layout/fragment_web_view_login.xml @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_1line_text.xml b/app/src/main/res/layout/item_1line_text.xml new file mode 100755 index 000000000..b2af0e293 --- /dev/null +++ b/app/src/main/res/layout/item_1line_text.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_1line_text_and_del.xml b/app/src/main/res/layout/item_1line_text_and_del.xml new file mode 100755 index 000000000..2cc6daa2c --- /dev/null +++ b/app/src/main/res/layout/item_1line_text_and_del.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_app_log.xml b/app/src/main/res/layout/item_app_log.xml new file mode 100755 index 000000000..eb1c229fc --- /dev/null +++ b/app/src/main/res/layout/item_app_log.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_arrange_book.xml b/app/src/main/res/layout/item_arrange_book.xml new file mode 100755 index 000000000..48ae5a58a --- /dev/null +++ b/app/src/main/res/layout/item_arrange_book.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_bg_image.xml b/app/src/main/res/layout/item_bg_image.xml new file mode 100755 index 000000000..4bf9cf5fe --- /dev/null +++ b/app/src/main/res/layout/item_bg_image.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_book_file_import.xml b/app/src/main/res/layout/item_book_file_import.xml new file mode 100755 index 000000000..8f584c1a4 --- /dev/null +++ b/app/src/main/res/layout/item_book_file_import.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/layout/item_book_group_manage.xml b/app/src/main/res/layout/item_book_group_manage.xml new file mode 100755 index 000000000..c48e13e67 --- /dev/null +++ b/app/src/main/res/layout/item_book_group_manage.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_book_manga_edge.xml b/app/src/main/res/layout/item_book_manga_edge.xml new file mode 100755 index 000000000..d96df3616 --- /dev/null +++ b/app/src/main/res/layout/item_book_manga_edge.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/item_book_manga_page.xml b/app/src/main/res/layout/item_book_manga_page.xml new file mode 100755 index 000000000..b6de8ad5c --- /dev/null +++ b/app/src/main/res/layout/item_book_manga_page.xml @@ -0,0 +1,64 @@ + + + + + + + + + + +

    + * Instances of CharsetDetector DO NOT have or maintain + * state pertaining to a specific match or detect operation. + * The WILL be shared by multiple instances of CharsetDetector. + * They encapsulate const charset-specific information. + */ +abstract class CharsetRecognizer { + /** + * Get the IANA name of this charset. + * + * @return the charset name. + */ + abstract String getName(); + + /** + * Get the ISO language code for this charset. + * + * @return the language code, or null if the language cannot be determined. + */ + public String getLanguage() { + return null; + } + + /** + * Test the match of this charset with the input text data + * which is obtained via the CharsetDetector object. + * + * @param det The CharsetDetector, which contains the input text + * to be checked for being in this charset. + * @return A CharsetMatch object containing details of match + * with this charset, or null if there was no match. + */ + abstract CharsetMatch match(CharsetDetector det); + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/KF6Book.kt b/app/src/main/java/io/legado/app/lib/mobi/KF6Book.kt new file mode 100755 index 000000000..740a79870 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/KF6Book.kt @@ -0,0 +1,140 @@ +package io.legado.app.lib.mobi + +import io.legado.app.lib.mobi.entities.KF6Section +import io.legado.app.lib.mobi.entities.MobiEntryHeaders +import io.legado.app.lib.mobi.entities.NCX +import io.legado.app.lib.mobi.entities.TOC +import java.nio.CharBuffer + + +/** + * Kindle Format 6 Book + */ +class KF6Book( + pdbFile: PDBFile, + headers: MobiEntryHeaders, + kf8BoundaryOffset: Int, + resourceStart: Int +) : MobiBook(pdbFile, headers, kf8BoundaryOffset, resourceStart) { + + lateinit var sections: List + lateinit var sectionIdMap: LinkedHashMap> + + init { + processSections() + processNCX() + processSectionsMap() + } + + fun getResourceByHref(href: String): ByteArray? { + val recindex = href.substringAfter("recindex:").toIntOrNull() ?: return null + return getResource(recindex - 1).array() + } + + fun getSectionText(section: KF6Section): String { + val inputStream = getTextRecordInputStream() + val byteArray = ByteArray(section.length) + inputStream.skip(section.start.toLong()) + inputStream.read(byteArray) + return String(byteArray, charset) + } + + fun getSectionByHref(href: String): KF6Section? { + val index = getIndexByHref(href) + return sections.getOrNull(index) + } + + private fun processSectionsMap() { + sectionIdMap = linkedMapOf() + if (toc == null) { + return + } + fun fmap(item: TOC) { + val index = getIndexByHref(item.href) + if (index == -1) return + val array = sectionIdMap.getOrPut(index) { arrayListOf() } + array.add(item) + item.subitems?.forEach(::fmap) + } + toc!!.forEach(::fmap) + } + + private fun getIndexByHref(href: String): Int { + val filepos = href.substringAfter("filepos:").toIntOrNull() ?: return -1 + return sections.indexOfFirst { it.end > filepos } + } + + private fun processNCX() { + val ncx = getNCX() ?: return + fun fmap(item: NCX): TOC { + val filepos = item.offset!! + val href = "filepos:${filepos.toString().padStart(10, '0')}" + return TOC(item.label, href, item.children?.map(::fmap)) + } + toc = ncx.map(::fmap) + } + + private fun processSections() { + val sections = arrayListOf() + val pattern = mbpPagebreakRegex.toPattern() + val inputStream = getTextRecordInputStream() + val available = inputStream.available() + val reader = inputStream.reader(Charsets.ISO_8859_1) + var buffer = CharBuffer.allocate(4096) + reader.read(buffer) + buffer.flip() + val matcher = pattern.matcher(buffer) + var droppedOffset = 0 + var nextStart = 0 + var position = 0 + while (true) { + if (!matcher.find()) { + buffer.position(position) + if (buffer.limit() == buffer.capacity()) { + if (position > 0) { + buffer.compact() + } else { + val newBuf = CharBuffer.allocate(buffer.capacity() * 2) + newBuf.put(buffer) + buffer = newBuf + } + droppedOffset += position + position = 0 + } + if (reader.read(buffer) == -1) { + break + } + buffer.flip() + matcher.reset(buffer) + } else { + val last = sections.lastOrNull() + val index = sections.size + val start = nextStart + val end = matcher.start() + droppedOffset + nextStart = matcher.end() + droppedOffset + position = matcher.end() + val length = end - start + val href = "filepos:${start.toString().padStart(10, '0')}" + val section = KF6Section(index, start, end, length, href) + last?.next = section + sections.add(section) + } + } + if (nextStart > 0) { + val last = sections.lastOrNull() + val index = sections.size + val start = nextStart + val length = available - start + val href = "filepos:${start.toString().padStart(10, '0')}" + val section = KF6Section(index, start, available, length, href) + last?.next = section + sections.add(section) + } + this.sections = sections + } + + companion object { + val mbpPagebreakRegex = "(?i)<\\s*(?:mbp:)?pagebreak[^>]*>".toRegex() + } + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/KF8Book.kt b/app/src/main/java/io/legado/app/lib/mobi/KF8Book.kt new file mode 100755 index 000000000..3f09c89ae --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/KF8Book.kt @@ -0,0 +1,272 @@ +package io.legado.app.lib.mobi + +import io.legado.app.lib.mobi.entities.FdstHeader +import io.legado.app.lib.mobi.entities.Fragment +import io.legado.app.lib.mobi.entities.KF8Pos +import io.legado.app.lib.mobi.entities.KF8Resource +import io.legado.app.lib.mobi.entities.KF8Section +import io.legado.app.lib.mobi.entities.MobiEntryHeaders +import io.legado.app.lib.mobi.entities.NCX +import io.legado.app.lib.mobi.entities.Skeleton +import io.legado.app.lib.mobi.entities.TOC +import io.legado.app.lib.mobi.utils.readString +import io.legado.app.lib.mobi.utils.readUInt32 +import java.nio.ByteBuffer +import java.util.Locale + +/** + * Kindle Format 8 Book + */ +@Suppress("SpellCheckingInspection") +class KF8Book( + pdbFile: PDBFile, + headers: MobiEntryHeaders, + kf8BoundaryOffset: Int, + resourceStart: Int +) : MobiBook(pdbFile, headers, kf8BoundaryOffset, resourceStart) { + + private var fdstTableStarts: IntArray? = null + private var fdstTableEnds: IntArray? = null + private lateinit var skelTable: List + private lateinit var fragTable: List + private var kf8 = headers.kf8!! + lateinit var sections: List + lateinit var sectionIdMap: LinkedHashMap> + + init { + readFdstTable() + readSkelTable() + readFragTable() + processSections() + processNCX() + processSectionsMap() + } + + /** + * 建立 section id -> toc 的 map + */ + private fun processSectionsMap() { + sectionIdMap = linkedMapOf() + if (toc == null) { + return + } + fun fmap(item: TOC) { + val index = getIndexByHref(item.href) + if (index == -1) return + val array = sectionIdMap.getOrPut(index) { arrayListOf() } + array.add(item) + item.subitems?.forEach(::fmap) + } + toc!!.forEach(::fmap) + } + + fun parsePosURI(href: String): KF8Pos? { + val match = kindlePosRegex.find(href) ?: return null + val fid = match.groupValues[1].toInt(32) + val off = match.groupValues[2].toInt(32) + return KF8Pos(fid, off) + } + + private fun parseResourceURI(href: String): KF8Resource? { + val match = kindleResourceRegex.find(href) ?: return null + val resourceType = match.groupValues[1] + val id = match.groupValues[2].toInt(32) + val type = match.groupValues[3] + return KF8Resource(resourceType, id, type) + } + + fun getResourceByHref(href: String): ByteArray? { + val resource = parseResourceURI(href) ?: return null + if (resource.resourceType == "flow") return null + return getResource(resource.id - 1).array() + } + + fun getSectionByHref(href: String): KF8Section? { + val index = getIndexByHref(href) + return sections.getOrNull(index) + } + + private fun getIndexByHref(href: String): Int { + val pos = parsePosURI(href) ?: return -1 + return getIndexByFID(pos.fid) + } + + private fun getIndexByFID(fid: Int): Int { + return sections.indexOfFirst { + it.frags.any { frag -> + frag.index == fid + } + } + } + + fun getTextByHref(href: String, nextHref: String): String { + val pos = parsePosURI(href) ?: return "" + val nextPos = parsePosURI(nextHref) ?: return "" + val index = getIndexByFID(pos.fid) + val nextIndex = getIndexByFID(nextPos.fid) + val startFid = pos.fid + val endFid = if (index == nextIndex) nextPos.fid else Int.MAX_VALUE + val section = sections[index] + val skel = section.skeleton + val droppedFrags = section.frags.filter { it.index < startFid } + var droppedFragsLength = droppedFrags.sumOf { it.length } + val frags = section.frags.filter { it.index in startFid..endFid } + val length = skel.length + frags.sumOf { it.length } + val raw = getRaw(skel.offset, section.length) + val lastFragDroppedLength = + if (index == nextIndex) frags.last().length - nextPos.offset else 0 + val skeleton = ByteArray(length - pos.offset - lastFragDroppedLength) + var leftBytes = skeleton.size + raw.copyInto(skeleton, 0, 0, skel.length) + leftBytes -= skel.length + for ((i, frag) in frags.withIndex()) { + val isFirstFrag = i == 0 + val isLastFrag = i == frags.lastIndex + val insertOffset = frag.insertOffset - skel.offset - droppedFragsLength + val offset = skel.length + frag.offset + skeleton.copyInto( + skeleton, + insertOffset + frag.length - + (if (isFirstFrag) pos.offset else 0) - + (if (isLastFrag) lastFragDroppedLength else 0), + insertOffset, + skeleton.size - leftBytes + ) + raw.copyInto( + skeleton, + insertOffset, + offset + if (isFirstFrag) pos.offset else 0, + offset + frag.length - if (isLastFrag) lastFragDroppedLength else 0 + ) + leftBytes -= frag.length - + (if (isFirstFrag) pos.offset else 0) - + (if (isLastFrag && index == nextIndex) nextPos.offset else 0) + if (isFirstFrag) { + droppedFragsLength += pos.offset + } + } + return String(skeleton, charset) + } + + fun getSectionText(section: KF8Section): String { + val skel = section.skeleton + val frags = section.frags + val length = section.length + val raw = getRaw(skel.offset, length) + val skeleton = ByteArray(raw.size) + var leftBytes = raw.size + raw.copyInto(skeleton, 0, 0, skel.length) + leftBytes -= skel.length + for (frag in frags) { + val insertOffset = frag.insertOffset - skel.offset + val offset = skel.length + frag.offset + skeleton.copyInto( + skeleton, + insertOffset + frag.length, + insertOffset, + skeleton.size - leftBytes + ) + raw.copyInto(skeleton, insertOffset, offset, offset + frag.length) + leftBytes -= frag.length + } + return String(skeleton, charset) + } + + private fun getRaw(offset: Int, len: Int): ByteArray { + val inputStream = getTextRecordInputStream() + val byteArray = ByteArray(len) + inputStream.skip(offset.toLong()) + inputStream.read(byteArray) + return byteArray + } + + private fun processNCX() { + val ncx = getNCX() ?: return + fun fmap(item: NCX): TOC { + val (fid, off) = item.pos!! + val href = makePosURI(fid, off) + return TOC(item.label, href, item.children?.map(::fmap)) + } + toc = ncx.map(::fmap) + } + + private fun makePosURI(fid: Int, off: Int): String { + val encodedFid = fid.toString(32).uppercase(Locale.ROOT).padStart(4, '0') + val encodedOff = off.toString(32).uppercase(Locale.ROOT).padStart(10, '0') + return "kindle:pos:fid:$encodedFid:off:$encodedOff" + } + + private fun processSections() { + sections = skelTable.fold(arrayListOf()) { arr, skel -> + val last = arr.lastOrNull() + val index = arr.size + val fragStart = last?.fragEnd ?: 0 + val fragEnd = fragStart + skel.numFrag + val frags = fragTable.slice(fragStart.. + val tagMap = indexEntry.tagMap + Fragment( + indexEntry.label.toInt(), + fragData.cncx[tagMap[2].tagValues[0]], + tagMap[4].tagValues[0], + tagMap[6].tagValues[0], + tagMap[6].tagValues[1] + ) + } + } + + private fun readSkelTable() { + skelTable = getIndexData(kf8.skel).table.mapIndexed { index, indexEntry -> + val tagMap = indexEntry.tagMap + Skeleton( + index, + indexEntry.label, + tagMap[1].tagValues[0], + tagMap[6].tagValues[0], + tagMap[6].tagValues[1], + ) + } + } + + private fun readFdstTable() { + try { + val fdstBuffer = getRecord(kf8.fdst) + val fdstHeader = readFdstHeader(fdstBuffer) + fdstTableStarts = IntArray(fdstHeader.numEntries) + fdstTableEnds = IntArray(fdstHeader.numEntries) + fdstBuffer.position(12) + for (i in 0.. Charsets.UTF_8 + 1252 -> Charset.forName("windows-1252") + else -> error("unknown charset $charset") + } + + private val decompressor: Decompressor = when (val compression = palmdoc.compression) { + 1 -> PlainDecompressor() + 2 -> Lz77Decompressor(max(4096, palmdoc.recordSize)) + 17480 -> HuffcdicDecompressor(this, mobi) + else -> error("unknown compression $charset") + } + + @Suppress("UNCHECKED_CAST") + val metadata: MobiMetadata by lazy { + MobiMetadata( + mobi.uid.toString(), + exth["title"] as? String ?: mobi.title, + exth["creator"] as? List ?: emptyList(), + exth["publisher"] as? String ?: "", + exth["language"] as? String ?: mobi.languege, + exth["date"] as? String ?: "", + exth["description"] as? String ?: "", + exth["subject"] as? List ?: emptyList(), + exth["rights"] as? String ?: "" + ) + } + + var toc: List? = null + + private var textRecordOffsets = arrayListOf() + + init { + buildTextRecordOffsets() + } + + private fun buildTextRecordOffsets() { + var offset = 0 + for (i in 0..= palmdoc.numTextRecords) { + throw IndexOutOfBoundsException("Text record index out of bounds") + } + var content = getRecord(index + 1).array() + content = removeTrailingEntries(content) + return decompressor.decompress(content) + } + + fun getTextRecordInputStream(): InputStream { + return object : InputStream() { + + private var index = -1 + private var bis: ByteArrayInputStream = emptyByteArrayInputStream + private var available = textRecordOffsets.last() + private var pos = 0 + + override fun read(): Int { + if (index >= palmdoc.numTextRecords) { + return -1 + } + + if (bis.available() == 0) { + if (++index >= palmdoc.numTextRecords) { + return -1 + } + bis = getTextRecord(index).inputStream() + } + + val b = bis.read() + + available-- + pos++ + + return b + } + + override fun skip(n: Long): Long { + if (n == 0L) return 0L + + val n1 = min(available, n.toInt()) + + if (n1 < bis.available()) { + bis.skip(n1.toLong()) + + available -= n1 + pos += n1 + + return n1.toLong() + } + + val bIndex = textRecordOffsets.binarySearch(pos + n1) + index = abs(bIndex + 1) + + bis = getTextRecord(index).inputStream() + + val offset = textRecordOffsets.getOrNull(index - 1) ?: 0 + bis.skip((pos + n1 - offset).toLong()) + + available -= n1 + pos += n1 + + return n1.toLong() + } + + override fun available(): Int { + return available + } + + } + } + + private fun removeTrailingEntries(byteArray: ByteArray): ByteArray { + if (trailingFlags == 0) return byteArray + val multibyte = trailingFlags and 1 != 0 + val numTrailingEntries = (trailingFlags shr 1).countOneBits() + val lastIndex = byteArray.lastIndex + var extraSize = 0 + for (i in 0..? { + val indxIndex = mobi.indx + if (indxIndex == -1) { + return null + } + val indexData = getIndexData(indxIndex) + val items = indexData.table.mapIndexed { index, indexEntry -> + val tagMap = indexEntry.tagMap + NCX( + index, + tagMap[1]?.tagValues?.getOrNull(0), + tagMap[2]?.tagValues?.getOrNull(0), + indexData.cncx[tagMap[3].tagValues[0]], + tagMap[4]?.tagValues?.getOrNull(0), + tagMap[6]?.tagValues, + tagMap[21]?.tagValues?.getOrNull(0), + tagMap[22]?.tagValues?.getOrNull(0), + tagMap[23]?.tagValues?.getOrNull(0), + ) + } + + val parentItemMap = hashMapOf>() + + items.forEach { + val parent = it.parent ?: return@forEach + val array = parentItemMap.getOrPut(parent) { arrayListOf() } + array.add(it) + } + + fun getChildren(item: NCX): NCX { + if (item.firstChild == null) return item + item.children = parentItemMap[item.index]?.map(::getChildren) + return item + } + + return items.filter { it.headingLevel == 0 }.map(::getChildren) + } + + fun getCover(): ByteArray? { + val coverOffset = exth["coverOffset"] as? Int + val thumbnailOffset = exth["thumbnailOffset"] as? Int + + if (coverOffset != null && coverOffset != -1) { + return getResource(coverOffset).array() + } + + if (thumbnailOffset != null && thumbnailOffset != -1) { + return getResource(thumbnailOffset).array() + } + + return null + } + + fun getIndexData(indxIndex: Int): IndexData { + val indxRecord = getRecord(indxIndex) + val indx = readIndxHeader(indxRecord) + indxRecord.position(indx.length) + val tagxBuffer = indxRecord.slice() + val tagx = readTagxHeader(tagxBuffer) + val tagTable = readTagxTags(tagx, tagxBuffer) + val cncx = readCncx(indxIndex, indx) + + val table = arrayListOf() + + for (i in 0.., + idxtOffset: Int + ): IndexEntry { + val array = indxBuffer.array() + + val len = indxBuffer.readUInt8(idxtOffset) + val label = indxBuffer.readString(idxtOffset + 1, len) + + val ptagxs = arrayListOf() + val startPos = idxtOffset + 1 + len + var controlByteIndex = 0 + var pos = startPos + tagx.numControlBytes + + for (tag in tagTable) { + if (tag.controlByte == 1) { + controlByteIndex++ + continue + } + val offset = startPos + controlByteIndex + var value = indxBuffer.readUInt8(offset) and tag.bitmask + if (value == tag.bitmask) { + if (tag.bitmask.countOneBits() > 1) { + var v = 0 + for (a in pos..() + val tagMap = SparseArray() + + for (ptagx in ptagxs) { + val values = arrayListOf() + if (ptagx.valueCount != null) { + repeat(ptagx.valueCount * ptagx.tagValueCount) { + var v = 0 + for (a in pos.. { + val numTags = (tagx.length - 12) / 4 + val tags = arrayListOf() + tagxBuffer.position(12) + for (i in 0.. { + val cncx = SparseArray() + var cncxRecordOffset = 0 + for (i in 0..= 8 + + var kf8BoundaryOffset = 0 + + if (!isKF8) { + val boundary = exth["boundary"] as? Int + if (boundary != null && boundary != -1) { + try { + val buffer = pdbFile.getRecordData(boundary) + mobiEntryHeaders = readMobiEntryHeaders(buffer) + kf8BoundaryOffset = boundary + isKF8 = true + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + return if (isKF8) { + KF8Book(pdbFile, mobiEntryHeaders, kf8BoundaryOffset, resourceStart) + } else { + KF6Book(pdbFile, mobiEntryHeaders, kf8BoundaryOffset, resourceStart) + } + } + + private fun readMobiEntryHeaders(buffer: ByteBuffer): MobiEntryHeaders { + val palmDocHeader = readPalmDocHeader(buffer) + val mobiHeader = readMobiHeader(buffer) + val exth = if (mobiHeader.exthFlag and 0b100_0000 != 0) { + buffer.position(mobiHeader.length + 16) + readExth(buffer.slice()) + } else { + emptyMap() + } + val kF8Header = if (mobiHeader.version >= 8) { + readKF8Header(buffer) + } else { + null + } + return MobiEntryHeaders(palmDocHeader, mobiHeader, exth, kF8Header) + } + + private fun readExth(buffer: ByteBuffer): Map { + val magic = buffer.readString(0, 4) + check(magic == "EXTH") { "Invalid EXTH header" } + val count = buffer.readUInt32(8) + var offset = 12 + val map = HashMap() + for (i in 0..() + } + @Suppress("UNCHECKED_CAST") + val array = map[name] as ArrayList + array.add(data as String) + } else { + map[name] = data + } + } + offset += length + } + return map + } + + private fun readPalmDocHeader(content: ByteBuffer): PalmDocHeader { + + val compression = content.readUInt16(0) + val numTextRecords = content.readUInt16(8) + val recordSize = content.readUInt16(10) + val encryption = content.readUInt16(12) + + return PalmDocHeader(compression, numTextRecords, recordSize, encryption) + } + + private fun readMobiHeader(content: ByteBuffer): MobiHeader { + + val identifier = content.readString(16, 4) + + check(identifier == "MOBI") { "Missing MOBI header" } + + val length = content.readUInt32(20) + val type = content.readUInt32(24) + val encoding = content.readUInt32(28) + val uid = content.readUInt32(32) + val version = content.readUInt32(36) + val titleOffset = content.readUInt32(84) + val titleLength = content.readUInt32(88) + val localeRegion = content.readUInt8(94) + val localeLanguage = content.readUInt8(95) + val resourceStar = content.readUInt32(108) + val huffcdic = content.readUInt32(112) + val numHuffcdic = content.readUInt32(116) + val exthFlag = content.readUInt32(128) + val trailingFlags = content.readUInt32(240) + val indx = content.readUInt32(244) + val charset: Charset = when (encoding) { + 65001 -> Charsets.UTF_8 + 1252 -> Charset.forName("windows-1252") + else -> error("unknown charset $encoding") + } + val title = content.readString(titleOffset, titleLength, charset) + + val lang = mobiLangMap[localeLanguage] + val language = lang?.getOrNull(localeRegion shr 2) ?: lang?.first() ?: "" + + return MobiHeader( + identifier, length, type, encoding, uid, version, titleOffset, titleLength, + localeRegion, localeLanguage, resourceStar, huffcdic, numHuffcdic, exthFlag, + trailingFlags, indx, title, language + ) + } + + private fun readKF8Header(content: ByteBuffer): KF8Header { + val fdst = content.readUInt32(192) + val numFdst = content.readUInt32(196) + val frag = content.readUInt32(248) + val skel = content.readUInt32(252) + val guide = content.readUInt32(260) + + return KF8Header(fdst, numFdst, frag, skel, guide) + } + + companion object { + val exthRecordTypeMap = mapOf( + 100 to ExthRecordType("creator", "string", true), + 101 to ExthRecordType("publisher"), + 103 to ExthRecordType("description"), + 104 to ExthRecordType("isbn"), + 105 to ExthRecordType("subject", "string", true), + 106 to ExthRecordType("date"), + 108 to ExthRecordType("contributor", "string", true), + 109 to ExthRecordType("rights"), + 110 to ExthRecordType("subjectCode", "string", true), + 112 to ExthRecordType("source", "string", true), + 113 to ExthRecordType("asin"), + 121 to ExthRecordType("boundary", "uint"), + 122 to ExthRecordType("fixedLayout"), + 125 to ExthRecordType("numResources", "uint"), + 126 to ExthRecordType("originalResolution"), + 127 to ExthRecordType("zeroGutter"), + 128 to ExthRecordType("zeroMargin"), + 129 to ExthRecordType("coverURI"), + 132 to ExthRecordType("regionMagnification"), + 201 to ExthRecordType("coverOffset", "uint"), + 202 to ExthRecordType("thumbnailOffset", "uint"), + 204 to ExthRecordType("creatorSoftware", "uint"), + 503 to ExthRecordType("title"), + 524 to ExthRecordType("language", "string", true), + 527 to ExthRecordType("pageProgressionDirection"), + ) + + val mobiLangMap = mapOf( + 1 to listOf( + "ar", "ar-SA", "ar-IQ", "ar-EG", "ar-LY", "ar-DZ", "ar-MA", "ar-TN", "ar-OM", + "ar-YE", "ar-SY", "ar-JO", "ar-LB", "ar-KW", "ar-AE", "ar-BH", "ar-QA" + ), + 2 to listOf("bg"), 3 to listOf("ca"), + 4 to listOf("zh", "zh-TW", "zh-CN", "zh-HK", "zh-SG"), + 5 to listOf("cs"), 6 to listOf("da"), + 7 to listOf("de", "de-DE", "de-CH", "de-AT", "de-LU", "de-LI"), 8 to listOf("el"), + 9 to listOf( + "en", "en-US", "en-GB", "en-AU", "en-CA", "en-NZ", "en-IE", "en-ZA", + "en-JM", null, "en-BZ", "en-TT", "en-ZW", "en-PH" + ), + 10 to listOf( + "es", "es-ES", "es-MX", null, "es-GT", "es-CR", "es-PA", "es-DO", + "es-VE", "es-CO", "es-PE", "es-AR", "es-EC", "es-CL", "es-UY", "es-PY", + "es-BO", "es-SV", "es-HN", "es-NI", "es-PR" + ), + 11 to listOf("fi"), + 12 to listOf("fr", "fr-FR", "fr-BE", "fr-CA", "fr-CH", "fr-LU", "fr-MC"), + 13 to listOf("he"), 14 to listOf("hu"), 15 to listOf("is"), + 16 to listOf("it", "it-IT", "it-CH"), 17 to listOf("ja"), 18 to listOf("ko"), + 19 to listOf("nl", "nl-NL", "nl-BE"), 20 to listOf("no", "nb", "nn"), + 21 to listOf("pl"), 22 to listOf("pt", "pt-BR", "pt-PT"), 23 to listOf("rm"), + 24 to listOf("ro"), 25 to listOf("ru"), 26 to listOf("hr", null, "sr"), + 27 to listOf("sk"), 28 to listOf("sq"), 29 to listOf("sv", "sv-SE", "sv-FI"), + 30 to listOf("th"), 31 to listOf("tr"), 32 to listOf("ur"), 33 to listOf("id"), + 34 to listOf("uk"), 35 to listOf("be"), 36 to listOf("sl"), 37 to listOf("et"), + 38 to listOf("lv"), 39 to listOf("lt"), 41 to listOf("fa"), 42 to listOf("vi"), + 43 to listOf("hy"), 44 to listOf("az"), 45 to listOf("eu"), 46 to listOf("hsb"), + 47 to listOf("mk"), 48 to listOf("st"), 49 to listOf("ts"), 50 to listOf("tn"), + 52 to listOf("xh"), 53 to listOf("zu"), 54 to listOf("af"), 55 to listOf("ka"), + 56 to listOf("fo"), 57 to listOf("hi"), 58 to listOf("mt"), 59 to listOf("se"), + 62 to listOf("ms"), 63 to listOf("kk"), 65 to listOf("sw"), + 67 to listOf("uz", null, "uz-UZ"), 68 to listOf("tt"), 69 to listOf("bn"), + 70 to listOf("pa"), 71 to listOf("gu"), 72 to listOf("or"), 73 to listOf("ta"), + 74 to listOf("te"), 75 to listOf("kn"), 76 to listOf("ml"), 77 to listOf("as"), + 78 to listOf("mr"), 79 to listOf("sa"), 82 to listOf("cy", "cy-GB"), + 83 to listOf("gl", "gl-ES"), 87 to listOf("kok"), 97 to listOf("ne"), + 98 to listOf("fy") + ) + } + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/PDBFile.kt b/app/src/main/java/io/legado/app/lib/mobi/PDBFile.kt new file mode 100755 index 000000000..dfa0c739f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/PDBFile.kt @@ -0,0 +1,49 @@ +package io.legado.app.lib.mobi + +import android.os.ParcelFileDescriptor +import io.legado.app.lib.mobi.utils.readString +import io.legado.app.lib.mobi.utils.readUInt16 +import io.legado.app.lib.mobi.utils.readUInt32 +import java.io.FileInputStream +import java.nio.ByteBuffer +import java.nio.channels.FileChannel + +class PDBFile(private val pfd: ParcelFileDescriptor) { + private val fc: FileChannel = FileInputStream(pfd.fileDescriptor).channel + private val offsets: IntArray + val name: String + val type: String + val creator: String + val recordCount: Int + + init { + var buffer = ByteBuffer.allocate(79) + fc.read(buffer) + name = buffer.readString(0, 32) + type = buffer.readString(60, 4) + creator = buffer.readString(64, 4) + recordCount = buffer.readUInt16(76) + + buffer = ByteBuffer.allocate(recordCount * 8) + fc.read(buffer, 78) + offsets = IntArray(recordCount) { + buffer.readUInt32(it * 8) + } + } + + fun getRecordData(index: Int): ByteBuffer { + if (index < 0 || index >= recordCount) { + throw IndexOutOfBoundsException("Record index out of bounds") + } + val len = offsets.getOrElse(index + 1) { fc.size().toInt() } - offsets[index] + val buffer = ByteBuffer.allocate(len) + fc.read(buffer, offsets[index].toLong()) + return buffer + } + + fun close() { + fc.close() + pfd.close() + } + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/decompress/CDICData.kt b/app/src/main/java/io/legado/app/lib/mobi/decompress/CDICData.kt new file mode 100755 index 000000000..980dd4fa2 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/decompress/CDICData.kt @@ -0,0 +1,6 @@ +package io.legado.app.lib.mobi.decompress + +class CDICEntry( + var data: ByteArray, + var decompressed: Boolean +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/decompress/Decompressor.kt b/app/src/main/java/io/legado/app/lib/mobi/decompress/Decompressor.kt new file mode 100755 index 000000000..a19352bb7 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/decompress/Decompressor.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.mobi.decompress + +interface Decompressor { + + fun decompress(data: ByteArray): ByteArray + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/decompress/HuffcdicDecompressor.kt b/app/src/main/java/io/legado/app/lib/mobi/decompress/HuffcdicDecompressor.kt new file mode 100755 index 000000000..d7f0a1df0 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/decompress/HuffcdicDecompressor.kt @@ -0,0 +1,133 @@ +package io.legado.app.lib.mobi.decompress + +import io.legado.app.lib.mobi.MobiBook +import io.legado.app.lib.mobi.entities.MobiHeader +import io.legado.app.lib.mobi.utils.readIntArray +import io.legado.app.lib.mobi.utils.readString +import io.legado.app.lib.mobi.utils.readUInt16 +import io.legado.app.lib.mobi.utils.readUInt32 +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import kotlin.math.min + +@Suppress("SpellCheckingInspection") +class HuffcdicDecompressor( + mobiBook: MobiBook, + mobiHeader: MobiHeader, +) : Decompressor { + + private val magic: String + private val offset1: Int + private val offset2: Int + private val table1: IntArray + private val mincodeTable = LongArray(33) + private val maxcodeTable = LongArray(33) + private val dictionary = arrayListOf() + + init { + val huff = mobiBook.getRecord(mobiHeader.huffcdic) + magic = huff.readString(0, 4) + if (magic != "HUFF") error("Invalid HUFF record") + offset1 = huff.readUInt32(8) + offset2 = huff.readUInt32(12) + + table1 = huff.readIntArray(offset1, 256) + + huff.position(offset2) + + for (i in 1..32) { + val mincode = huff.readUInt32().toLong() + val maxcode = huff.readUInt32().toLong() + mincodeTable[i] = mincode shl (32 - i) + maxcodeTable[i] = ((maxcode + 1) shl (32 - i)) - 1 + } + + for (i in 1.. 0 && bytesLeft-- > 0) { + value = value or ((get().toLong() and 0xFF) shl (i * 8)) + } + return value + } + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/decompress/Lz77Decompressor.kt b/app/src/main/java/io/legado/app/lib/mobi/decompress/Lz77Decompressor.kt new file mode 100755 index 000000000..91ed22b01 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/decompress/Lz77Decompressor.kt @@ -0,0 +1,49 @@ +package io.legado.app.lib.mobi.decompress + +import androidx.core.util.Pools.SynchronizedPool + +class Lz77Decompressor(private val textRecordSize: Int) : Decompressor { + + val pool = SynchronizedPool(2) + + override fun decompress(data: ByteArray): ByteArray { + val out = pool.acquire() ?: ByteArray(textRecordSize) + + var i = 0 + var o = 0 + while (i < data.size) { + var c = data[i++].toInt() and 0x00FF + if (c in 0x01..0x08) { + var j = 0 + while (j < c && i + j < data.size) { + out[o++] = data[i + j] + j++ + } + i += c + } else if (c <= 0x7f) { + out[o++] = c.toByte() + } else if (c >= 0xC0) { + out[o++] = ' '.code.toByte() + out[o++] = (c xor 0x80).toByte() + } else { + if (i < data.size) { + c = c shl 8 or (data[i++].toInt() and 0xFF) + val length = (c and 0x0007) + 3 + val location = (c shr 3) and 0x7FF + + if (location in 1..o) { + for (j in 0 until length) { + val idx = o - location + out[o++] = out[idx] + } + } + } + } + } + + val result = ByteArray(o) + System.arraycopy(out, 0, result, 0, o) + return result.also { pool.release(out) } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/decompress/PlainDecompressor.kt b/app/src/main/java/io/legado/app/lib/mobi/decompress/PlainDecompressor.kt new file mode 100755 index 000000000..7bc4ce743 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/decompress/PlainDecompressor.kt @@ -0,0 +1,9 @@ +package io.legado.app.lib.mobi.decompress + +class PlainDecompressor : Decompressor { + + override fun decompress(data: ByteArray): ByteArray { + return data + } + +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/ExthRecordType.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/ExthRecordType.kt new file mode 100755 index 000000000..b2746d6be --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/ExthRecordType.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.mobi.entities + +data class ExthRecordType( + val name: String, + val type: String = "string", + val many: Boolean = false +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/FdstHeader.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/FdstHeader.kt new file mode 100755 index 000000000..61fb1b63e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/FdstHeader.kt @@ -0,0 +1,6 @@ +package io.legado.app.lib.mobi.entities + +data class FdstHeader( + val magic: String, + val numEntries: Int +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/Fragment.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/Fragment.kt new file mode 100755 index 000000000..50e647f6d --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/Fragment.kt @@ -0,0 +1,9 @@ +package io.legado.app.lib.mobi.entities + +data class Fragment( + val insertOffset: Int, + val selector: String, + val index: Int, + val offset: Int, + val length: Int +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/IndexData.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/IndexData.kt new file mode 100755 index 000000000..962d99efe --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/IndexData.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.mobi.entities + +import android.util.SparseArray + +data class IndexData( + val table: List, + val cncx: SparseArray +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/IndexEntry.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/IndexEntry.kt new file mode 100755 index 000000000..717a51997 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/IndexEntry.kt @@ -0,0 +1,9 @@ +package io.legado.app.lib.mobi.entities + +import android.util.SparseArray + +data class IndexEntry( + val label: String, + val tags: List, + val tagMap: SparseArray +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/IndexTag.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/IndexTag.kt new file mode 100755 index 000000000..8de50d2b4 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/IndexTag.kt @@ -0,0 +1,6 @@ +package io.legado.app.lib.mobi.entities + +data class IndexTag( + val tagId: Int, + val tagValues: List +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/IndxHeader.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/IndxHeader.kt new file mode 100755 index 000000000..7c13bcc81 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/IndxHeader.kt @@ -0,0 +1,16 @@ +package io.legado.app.lib.mobi.entities + +data class IndxHeader( + val magic: String, + val length: Int, + val type: Int, + val idxt: Int, + val numRecords: Int, + val encoding: Int, + val language: Int, + val total: Int, + val ordt: Int, + val ligt: Int, + val numLigt: Int, + val numCncx: Int, +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/KF6Section.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/KF6Section.kt new file mode 100755 index 000000000..b15ccf487 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/KF6Section.kt @@ -0,0 +1,10 @@ +package io.legado.app.lib.mobi.entities + +data class KF6Section( + val index: Int, + val start: Int, + val end: Int, + val length: Int, + val href: String, + var next: KF6Section? = null +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Header.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Header.kt new file mode 100755 index 000000000..c5c639b2e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Header.kt @@ -0,0 +1,9 @@ +package io.legado.app.lib.mobi.entities + +data class KF8Header( + val fdst: Int, + val numFdst: Int, + val frag: Int, + val skel: Int, + val guide: Int, +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Pos.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Pos.kt new file mode 100755 index 000000000..e317a76ca --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Pos.kt @@ -0,0 +1,6 @@ +package io.legado.app.lib.mobi.entities + +data class KF8Pos( + val fid: Int, + val offset: Int +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Resource.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Resource.kt new file mode 100755 index 000000000..eb2b51d79 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Resource.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.mobi.entities + +data class KF8Resource( + val resourceType: String, + val id: Int, + val type: String +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Section.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Section.kt new file mode 100755 index 000000000..781cf9f33 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/KF8Section.kt @@ -0,0 +1,14 @@ +package io.legado.app.lib.mobi.entities + +data class KF8Section( + val index: Int, + val skeleton: Skeleton, + val frags: List, + val fragEnd: Int, + val length: Int, + val totalLength: Int, + val href: String, + var next: KF8Section? = null +) { + val linear get() = frags.isNotEmpty() +} diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/MobiEntryHeaders.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/MobiEntryHeaders.kt new file mode 100755 index 000000000..0194dbdb9 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/MobiEntryHeaders.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.mobi.entities + +data class MobiEntryHeaders( + val palmdoc: PalmDocHeader, + val mobi: MobiHeader, + val exth: Map, + val kf8: KF8Header? +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/MobiHeader.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/MobiHeader.kt new file mode 100755 index 000000000..827f1eaae --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/MobiHeader.kt @@ -0,0 +1,22 @@ +package io.legado.app.lib.mobi.entities + +data class MobiHeader( + val identifier: String, + val length: Int, + val type: Int, + val encoding: Int, + val uid: Int, + val version: Int, + val titleOffset: Int, + val titleLength: Int, + val localeRegion: Int, + val localeLanguage: Int, + val resourceStart: Int, + val huffcdic: Int, + val numHuffcdic: Int, + val exthFlag: Int, + val trailingFlags: Int, + val indx: Int, + val title: String, + val languege: String +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/MobiMetadata.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/MobiMetadata.kt new file mode 100755 index 000000000..5655e2704 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/MobiMetadata.kt @@ -0,0 +1,13 @@ +package io.legado.app.lib.mobi.entities + +data class MobiMetadata( + val identifier: String, + val title: String, + val author: List, + val publisher: String, + val language: String, + val published: String, + val description: String, + val subject: List, + val rights: String +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/NCX.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/NCX.kt new file mode 100755 index 000000000..4aed57f83 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/NCX.kt @@ -0,0 +1,14 @@ +package io.legado.app.lib.mobi.entities + +data class NCX( + val index: Int, + val offset: Int?, + val size: Int?, + val label: String, + val headingLevel: Int?, + val pos: List?, + val parent: Int?, + val firstChild: Int?, + val lastChild: Int?, + var children: List? = null +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/PalmDocHeader.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/PalmDocHeader.kt new file mode 100755 index 000000000..c985a02fc --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/PalmDocHeader.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.mobi.entities + +data class PalmDocHeader( + val compression: Int, + val numTextRecords: Int, + val recordSize: Int, + val encryption: Int +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/Ptagx.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/Ptagx.kt new file mode 100755 index 000000000..adb2a3d01 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/Ptagx.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.mobi.entities + +data class Ptagx( + val tag: Int, + val tagValueCount: Int, + val valueCount: Int?, + val valueBytes: Int? +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/Skeleton.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/Skeleton.kt new file mode 100755 index 000000000..cd6979687 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/Skeleton.kt @@ -0,0 +1,9 @@ +package io.legado.app.lib.mobi.entities + +data class Skeleton( + val index: Int, + val name: String, + val numFrag: Int, + val offset: Int, + val length: Int +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/TOC.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/TOC.kt new file mode 100755 index 000000000..d47f31f39 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/TOC.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.mobi.entities + +data class TOC( + val label: String, + val href: String, + val subitems: List? = null +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/TagxHeader.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/TagxHeader.kt new file mode 100755 index 000000000..0925ddb27 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/TagxHeader.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.mobi.entities + +data class TagxHeader( + val magic: String, + val length: Int, + val numControlBytes: Int +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/entities/TagxTag.kt b/app/src/main/java/io/legado/app/lib/mobi/entities/TagxTag.kt new file mode 100755 index 000000000..2b02edf7c --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/entities/TagxTag.kt @@ -0,0 +1,8 @@ +package io.legado.app.lib.mobi.entities + +data class TagxTag( + val tag: Int, + val numValues: Int, + val bitmask: Int, + val controlByte: Int, +) diff --git a/app/src/main/java/io/legado/app/lib/mobi/utils/BitwiseExtensions.kt b/app/src/main/java/io/legado/app/lib/mobi/utils/BitwiseExtensions.kt new file mode 100755 index 000000000..aa1725cb6 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/utils/BitwiseExtensions.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.mobi.utils + +internal infix fun Byte.and(mask: Int): Int = toInt() and mask + +internal infix fun Short.and(mask: Int): Int = toInt() and mask + +internal infix fun Int.and(mask: Long): Long = toLong() and mask diff --git a/app/src/main/java/io/legado/app/lib/mobi/utils/ByteBufferExtensions.kt b/app/src/main/java/io/legado/app/lib/mobi/utils/ByteBufferExtensions.kt new file mode 100755 index 000000000..e206d6f3f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/mobi/utils/ByteBufferExtensions.kt @@ -0,0 +1,72 @@ +package io.legado.app.lib.mobi.utils + +import java.nio.ByteBuffer +import java.nio.charset.Charset + +fun ByteBuffer.readByteArray(offset: Int, len: Int): ByteArray { + position(offset) + val b = ByteArray(len) + get(b) + return b +} + +fun ByteBuffer.readByteArray(len: Int): ByteArray { + val b = ByteArray(len) + get(b) + return b +} + +fun ByteBuffer.readIntArray(offset: Int, len: Int): IntArray { + position(offset) + return IntArray(len) { + getInt() + } +} + +fun ByteBuffer.readUInt16Array(offset: Int, len: Int): IntArray { + position(offset) + return IntArray(len) { + getShort() and 0xFFFF + } +} + +fun ByteBuffer.readString(len: Int): String { + return String(readByteArray(len)) +} + +fun ByteBuffer.readString(offset: Int, len: Int): String { + return String(readByteArray(offset, len)) +} + +fun ByteBuffer.readString(offset: Int, len: Int, charset: Charset): String { + return String(readByteArray(offset, len), charset) +} + +fun ByteBuffer.readUInt8(offset: Int): Int { + position(offset) + return get() and 0xFF +} + +fun ByteBuffer.readUInt8(): Int { + return get() and 0xFF +} + +fun ByteBuffer.readUInt16(offset: Int): Int { + position(offset) + return getShort() and 0xFFFF +} + +fun ByteBuffer.readUInt32(offset: Int): Int { + position(offset) + return getInt() +} + +fun ByteBuffer.readUInt32(): Int { + return getInt() +} + +fun ByteBuffer.readUInt64(offset: Int): Long { + position(offset) + return getLong() +} + diff --git a/app/src/main/java/io/legado/app/lib/permission/OnErrorCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnErrorCallback.kt new file mode 100755 index 000000000..2f32126a9 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnErrorCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.permission + +interface OnErrorCallback { + + fun onError(e: Exception) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt new file mode 100755 index 000000000..0cc6b715b --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsDeniedCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.permission + +interface OnPermissionsDeniedCallback { + + fun onPermissionsDenied(deniedPermissions: Array) + +} diff --git a/app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt new file mode 100755 index 000000000..fe43e947e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsGrantedCallback.kt @@ -0,0 +1,7 @@ +package io.legado.app.lib.permission + +interface OnPermissionsGrantedCallback { + + fun onPermissionsGranted() + +} diff --git a/app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt new file mode 100755 index 000000000..9d83c31af --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnPermissionsResultCallback.kt @@ -0,0 +1,11 @@ +package io.legado.app.lib.permission + +interface OnPermissionsResultCallback { + + fun onPermissionsGranted() + + fun onPermissionsDenied(deniedPermissions: Array?) + + fun onError(e: Exception) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt b/app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt new file mode 100755 index 000000000..4298e0fb4 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/OnRequestPermissionsResultCallback.kt @@ -0,0 +1,10 @@ +package io.legado.app.lib.permission + +interface OnRequestPermissionsResultCallback { + + fun onRequestPermissionsResult(permissions: Array, grantResults: IntArray) + + fun onSettingActivityResult() + + fun onError(e: Exception) +} diff --git a/app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt b/app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt new file mode 100755 index 000000000..5bd05ac1c --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/PermissionActivity.kt @@ -0,0 +1,227 @@ +package io.legado.app.lib.permission + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.activity.addCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.exception.NoStackTraceException +import io.legado.app.utils.registerForActivityResult +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.launch + +class PermissionActivity : AppCompatActivity() { + + private var rationaleDialog: AlertDialog? = null + + private val settingActivityResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + onRequestPermissionFinish() + } + private val settingActivityResultAwait = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) + private val requestPermissionResult = + registerForActivityResult(ActivityResultContracts.RequestPermission()) + private val requestPermissionsResult = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) + + @SuppressLint("BatteryLife") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val rationale = intent.getStringExtra(KEY_RATIONALE) + val requestCode = intent.getIntExtra(KEY_INPUT_PERMISSIONS_CODE, 1000) + val permissions = intent.getStringArrayExtra(KEY_INPUT_PERMISSIONS)!! + when (intent.getIntExtra(KEY_INPUT_REQUEST_TYPE, Request.TYPE_REQUEST_PERMISSION)) { + //权限请求 + Request.TYPE_REQUEST_PERMISSION -> showSettingDialog(permissions, rationale) { + lifecycleScope.launch { + try { + val result = requestPermissionsResult.launch(permissions) + if (result.values.all { it }) { + onRequestPermissionFinish() + } else { + openSettingsActivity() + } + } catch (e: Exception) { + AppLog.put("请求权限出错\n$e", e, true) + RequestPlugins.sRequestCallback?.onError(e) + finish() + } + } + } + //跳转到设置界面 + Request.TYPE_REQUEST_SETTING -> showSettingDialog(permissions, rationale) { + openSettingsActivity() + } + //所有文件的管理权限 + Request.TYPE_MANAGE_ALL_FILES_ACCESS -> showSettingDialog(permissions, rationale) { + try { + if (Permissions.isManageExternalStorage()) { + val settingIntent = + Intent( + Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, + Uri.parse("package:$packageName") + ) + settingActivityResult.launch(settingIntent) + } else { + throw NoStackTraceException("no MANAGE_ALL_FILES_ACCESS_PERMISSION") + } + } catch (e: Exception) { + AppLog.put("请求所有文件的管理权限出错\n$e", e, true) + RequestPlugins.sRequestCallback?.onError(e) + finish() + } + } + + Request.TYPE_REQUEST_NOTIFICATIONS -> showSettingDialog(permissions, rationale) { + lifecycleScope.launch { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && requestPermissionResult.launch(Permissions.POST_NOTIFICATIONS) + ) { + onRequestPermissionFinish() + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + //这种方案适用于 API 26, 即8.0(含8.0)以上可以用 + val intent = Intent() + intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + intent.putExtra(Settings.EXTRA_CHANNEL_ID, applicationInfo.uid) + settingActivityResult.launch(intent) + } else { + openSettingsActivity() + } + } catch (e: Exception) { + AppLog.put("请求通知权限出错\n$e", e, true) + RequestPlugins.sRequestCallback?.onError(e) + finish() + } + } + } + + Request.TYPE_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS -> showSettingDialog( + permissions, rationale + ) { + lifecycleScope.launch { + try { + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + intent.setData(Uri.parse("package:$packageName")) + val className = + "com.android.settings.fuelgauge.RequestIgnoreBatteryOptimizations" + val activities = packageManager.queryIntentActivities( + intent, + PackageManager.MATCH_DEFAULT_ONLY + ) + if (activities.any { it.activityInfo.name == className }) { + val component = intent.resolveActivity(packageManager) + if (component.className != className) { + intent.setClassName("com.android.settings", className) + settingActivityResultAwait.launch(intent) + } + } + intent.component = null + settingActivityResult.launch(intent) + } catch (e: Exception) { + AppLog.put("请求后台权限出错\n$e", e, true) + RequestPlugins.sRequestCallback?.onError(e) + finish() + } + } + } + } + onBackPressedDispatcher.addCallback(this) { + + } + } + + private fun onRequestPermissionFinish() { + RequestPlugins.sRequestCallback?.onSettingActivityResult() + finish() + } + + private fun openSettingsActivity() { + try { + val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + settingIntent.data = Uri.fromParts("package", packageName, null) + settingActivityResult.launch(settingIntent) + } catch (e: Exception) { + toastOnUi(R.string.tip_cannot_jump_setting_page) + RequestPlugins.sRequestCallback?.onError(e) + finish() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + RequestPlugins.sRequestCallback?.onRequestPermissionsResult( + permissions, + grantResults + ) + finish() + } + + + override fun startActivity(intent: Intent) { + super.startActivity(intent) + @Suppress("DEPRECATION") + overridePendingTransition(0, 0) + } + + override fun finish() { + super.finish() + @Suppress("DEPRECATION") + overridePendingTransition(0, 0) + } + + private fun showSettingDialog( + permissions: Array, + rationale: CharSequence?, + onOk: () -> Unit + ) { + rationaleDialog?.dismiss() + if (rationale.isNullOrEmpty()) { + finish() + return + } + rationaleDialog = AlertDialog.Builder(this) + .setTitle(R.string.dialog_title) + .setMessage(rationale) + .setPositiveButton(R.string.dialog_setting) { _, _ -> + onOk.invoke() + } + .setNegativeButton(R.string.dialog_cancel) { _, _ -> + RequestPlugins.sRequestCallback?.onRequestPermissionsResult( + permissions, + IntArray(0) + ) + finish() + }.setOnCancelListener { + RequestPlugins.sRequestCallback?.onRequestPermissionsResult( + permissions, + IntArray(0) + ) + finish() + } + .show() + } + + companion object { + + const val KEY_RATIONALE = "KEY_RATIONALE" + const val KEY_INPUT_REQUEST_TYPE = "KEY_INPUT_REQUEST_TYPE" + const val KEY_INPUT_PERMISSIONS_CODE = "KEY_INPUT_PERMISSIONS_CODE" + const val KEY_INPUT_PERMISSIONS = "KEY_INPUT_PERMISSIONS" + } +} diff --git a/app/src/main/java/io/legado/app/lib/permission/Permissions.kt b/app/src/main/java/io/legado/app/lib/permission/Permissions.kt new file mode 100755 index 000000000..4801b1dab --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/Permissions.kt @@ -0,0 +1,90 @@ +package io.legado.app.lib.permission + +import android.os.Build + +@Suppress("unused") +object Permissions { + + const val POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS" + + const val READ_CALENDAR = "android.permission.READ_CALENDAR" + const val WRITE_CALENDAR = "android.permission.WRITE_CALENDAR" + + const val CAMERA = "android.permission.CAMERA" + + const val READ_CONTACTS = "android.permission.READ_CONTACTS" + const val WRITE_CONTACTS = "android.permission.WRITE_CONTACTS" + const val GET_ACCOUNTS = "android.permission.GET_ACCOUNTS" + + const val ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION" + const val ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION" + + const val RECORD_AUDIO = "android.permission.RECORD_AUDIO" + + const val READ_PHONE_STATE = "android.permission.READ_PHONE_STATE" + const val CALL_PHONE = "android.permission.CALL_PHONE" + const val READ_CALL_LOG = "android.permission.READ_CALL_LOG" + const val WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG" + const val ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL" + const val USE_SIP = "android.permission.USE_SIP" + const val PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS" + + const val BODY_SENSORS = "android.permission.BODY_SENSORS" + + const val SEND_SMS = "android.permission.SEND_SMS" + const val RECEIVE_SMS = "android.permission.RECEIVE_SMS" + const val READ_SMS = "android.permission.READ_SMS" + const val RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH" + const val RECEIVE_MMS = "android.permission.RECEIVE_MMS" + + const val READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE" + const val WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE" + const val MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE" + + const val ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION" + + const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" + + object Group { + val STORAGE = if (isManageExternalStorage()) { + arrayOf(MANAGE_EXTERNAL_STORAGE) + } else { + arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) + } + + val CAMERA = arrayOf(Permissions.CAMERA) + + val CALENDAR = arrayOf(READ_CALENDAR, WRITE_CALENDAR) + + val CONTACTS = arrayOf(READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS) + + val LOCATION = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) + + val MICROPHONE = arrayOf(RECORD_AUDIO) + + val PHONE = arrayOf( + READ_PHONE_STATE, + CALL_PHONE, + READ_CALL_LOG, + WRITE_CALL_LOG, + ADD_VOICEMAIL, + USE_SIP, + PROCESS_OUTGOING_CALLS + ) + + val SENSORS = arrayOf(BODY_SENSORS) + + val SMS = arrayOf( + SEND_SMS, + RECEIVE_SMS, + READ_SMS, + RECEIVE_WAP_PUSH, + RECEIVE_MMS + ) + } + + fun isManageExternalStorage(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + } +} diff --git a/app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt b/app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt new file mode 100755 index 000000000..c29eb31d7 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/PermissionsCompat.kt @@ -0,0 +1,72 @@ +package io.legado.app.lib.permission + +import androidx.annotation.StringRes + +@Suppress("unused") +class PermissionsCompat private constructor() { + + private var request: Request? = null + + fun request() { + RequestManager.pushRequest(request) + } + + class Builder { + private val request: Request = Request() + + fun addPermissions(vararg permissions: String): Builder { + request.addPermissions(*permissions) + return this + } + + fun onGranted(callback: () -> Unit): Builder { + request.setOnGrantedCallback(object : OnPermissionsGrantedCallback { + override fun onPermissionsGranted() { + callback() + } + }) + return this + } + + fun onDenied(callback: (deniedPermissions: Array) -> Unit): Builder { + request.setOnDeniedCallback(object : OnPermissionsDeniedCallback { + override fun onPermissionsDenied(deniedPermissions: Array) { + callback(deniedPermissions) + } + }) + return this + } + + fun onError(callback: (e: Exception) -> Unit): Builder { + request.setOnErrorCallBack(object : OnErrorCallback{ + override fun onError(e: Exception) { + callback(e) + } + }) + return this + } + + fun rationale(rationale: CharSequence): Builder { + request.setRationale(rationale) + return this + } + + fun rationale(@StringRes resId: Int): Builder { + request.setRationale(resId) + return this + } + + fun build(): PermissionsCompat { + val compat = PermissionsCompat() + compat.request = request + return compat + } + + fun request(): PermissionsCompat { + val compat = build() + compat.request() + return compat + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/permission/Request.kt b/app/src/main/java/io/legado/app/lib/permission/Request.kt new file mode 100755 index 000000000..4ac8fabc1 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/Request.kt @@ -0,0 +1,228 @@ +package io.legado.app.lib.permission + +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.os.Build +import android.os.Environment +import androidx.annotation.StringRes +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import io.legado.app.utils.startActivity +import splitties.init.appCtx +import splitties.systemservices.powerManager + +@Suppress("MemberVisibilityCanBePrivate") +internal class Request : OnRequestPermissionsResultCallback { + + internal val requestTime: Long = System.currentTimeMillis() + private var requestCode: Int = TYPE_REQUEST_PERMISSION + private var permissions: ArrayList = ArrayList() + private var grantedCallback: OnPermissionsGrantedCallback? = null + private var deniedCallback: OnPermissionsDeniedCallback? = null + private var errorCallback: OnErrorCallback? = null + private var rationale: CharSequence? = null + + private val deniedPermissions: Array? + get() { + return getDeniedPermissions(this.permissions.toTypedArray()) + } + + fun addPermissions(vararg permissions: String) { + this.permissions.addAll(listOf(*permissions)) + } + + fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) { + grantedCallback = callback + } + + fun setOnDeniedCallback(callback: OnPermissionsDeniedCallback) { + deniedCallback = callback + } + + fun setOnErrorCallBack(callback: OnErrorCallback) { + errorCallback = callback + } + + fun setRationale(@StringRes resId: Int) { + rationale = appCtx.getString(resId) + } + + fun setRationale(rationale: CharSequence) { + this.rationale = rationale + } + + @SuppressLint("ObsoleteSdkInt") + fun start() { + RequestPlugins.setOnRequestPermissionsCallback(this) + + val deniedPermissions = deniedPermissions + val rationale = this.rationale + if (deniedPermissions == null) { + onPermissionsGranted() + return + } + if (rationale == null) { + onPermissionsDenied(deniedPermissions) + return + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + toSetting() + } else { + if (deniedPermissions.contains(Permissions.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + toManageFileSetting(deniedPermissions) + } + } else if (deniedPermissions.contains(Permissions.POST_NOTIFICATIONS)) { + toNotificationSetting(deniedPermissions) + } else if (deniedPermissions.contains(Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + toIgnoreBatterySetting(deniedPermissions) + } + } else if (deniedPermissions.isNotEmpty()) { + appCtx.startActivity { + putExtra(PermissionActivity.KEY_RATIONALE, rationale) + putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) + } + } + } + } + + fun clear() { + grantedCallback = null + deniedCallback = null + } + + fun getDeniedPermissions(permissions: Array): Array? { + val deniedPermissionList = ArrayList() + for (permission in permissions) { + when (permission) { + Permissions.POST_NOTIFICATIONS -> { + if (!NotificationManagerCompat.from(appCtx).areNotificationsEnabled()) { + deniedPermissionList.add(permission) + } + } + + Permissions.MANAGE_EXTERNAL_STORAGE -> { + if (Permissions.isManageExternalStorage()) { + if (!Environment.isExternalStorageManager()) { + deniedPermissionList.add(permission) + } + } + } + + Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!powerManager.isIgnoringBatteryOptimizations(appCtx.packageName)) { + deniedPermissionList.add(permission) + } + } + } + + else -> { + if ( + ContextCompat.checkSelfPermission(appCtx, permission) + != PackageManager.PERMISSION_GRANTED + ) { + deniedPermissionList.add(permission) + } + } + } + } + val size = deniedPermissionList.size + if (size > 0) { + return deniedPermissionList.toTypedArray() + } + return null + } + + private fun onPermissionsGranted() { + try { + grantedCallback?.onPermissionsGranted() + } catch (ignore: Exception) { + } + + RequestPlugins.sResultCallback?.onPermissionsGranted() + } + + private fun onPermissionsDenied(deniedPermissions: Array) { + try { + deniedCallback?.onPermissionsDenied(deniedPermissions) + } catch (ignore: Exception) { + } + + RequestPlugins.sResultCallback?.onPermissionsDenied(deniedPermissions) + } + + private fun toSetting() { + appCtx.startActivity { + putExtra(PermissionActivity.KEY_RATIONALE, rationale) + putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_SETTING) + } + } + + private fun toManageFileSetting(deniedPermissions: Array) { + appCtx.startActivity { + putExtra(PermissionActivity.KEY_RATIONALE, rationale) + putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_MANAGE_ALL_FILES_ACCESS) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) + } + } + + private fun toNotificationSetting(deniedPermissions: Array) { + appCtx.startActivity { + putExtra(PermissionActivity.KEY_RATIONALE, rationale) + putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_NOTIFICATIONS) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) + } + } + + private fun toIgnoreBatterySetting(deniedPermissions: Array) { + appCtx.startActivity { + putExtra(PermissionActivity.KEY_RATIONALE, rationale) + putExtra( + PermissionActivity.KEY_INPUT_REQUEST_TYPE, + TYPE_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + ) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode) + putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions) + } + } + + override fun onRequestPermissionsResult( + permissions: Array, + grantResults: IntArray + ) { + val deniedPermissions = getDeniedPermissions(permissions) + if (deniedPermissions != null) { + onPermissionsDenied(deniedPermissions) + } else { + onPermissionsGranted() + } + } + + override fun onSettingActivityResult() { + val deniedPermissions = deniedPermissions + if (deniedPermissions == null) { + onPermissionsGranted() + } else { + onPermissionsDenied(deniedPermissions) + } + } + + override fun onError(e: Exception) { + errorCallback?.onError(e) + RequestPlugins.sResultCallback?.onError(e) + } + + companion object { + const val TYPE_REQUEST_PERMISSION = 1 + const val TYPE_REQUEST_SETTING = 2 + const val TYPE_MANAGE_ALL_FILES_ACCESS = 3 + const val TYPE_REQUEST_NOTIFICATIONS = 4 + const val TYPE_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 5 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/permission/RequestManager.kt b/app/src/main/java/io/legado/app/lib/permission/RequestManager.kt new file mode 100755 index 000000000..c4a67edb8 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/RequestManager.kt @@ -0,0 +1,73 @@ +package io.legado.app.lib.permission + +import android.os.Handler +import android.os.Looper +import java.util.* + +internal object RequestManager : OnPermissionsResultCallback { + + private var requests: Stack? = null + private var request: Request? = null + + private val handler = Handler(Looper.getMainLooper()) + + private val requestRunnable = Runnable { + request?.start() + } + + private val isCurrentRequestInvalid: Boolean + get() = request?.let { System.currentTimeMillis() - it.requestTime > 5 * 1000L } ?: true + + init { + RequestPlugins.setOnPermissionsResultCallback(this) + } + + fun pushRequest(request: Request?) { + if (request == null) return + + if (requests == null) { + requests = Stack() + } + + requests?.let { + val index = it.indexOf(request) + if (index >= 0) { + val to = it.size - 1 + if (index != to) { + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + Collections.swap(requests, index, to) + } + } else { + it.push(request) + } + + if (!it.empty() && isCurrentRequestInvalid) { + this.request = it.pop() + handler.post(requestRunnable) + } + } + } + + private fun startNextRequest() { + request?.clear() + request = null + + requests?.let { + request = if (it.empty()) null else it.pop() + request?.let { handler.post(requestRunnable) } + } + } + + override fun onPermissionsGranted() { + startNextRequest() + } + + override fun onPermissionsDenied(deniedPermissions: Array?) { + startNextRequest() + } + + override fun onError(e: Exception) { + startNextRequest() + } + +} diff --git a/app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt b/app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt new file mode 100755 index 000000000..59bf0f0eb --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/permission/RequestPlugins.kt @@ -0,0 +1,20 @@ +package io.legado.app.lib.permission + +internal object RequestPlugins { + + @Volatile + var sRequestCallback: OnRequestPermissionsResultCallback? = null + + @Volatile + var sResultCallback: OnPermissionsResultCallback? = null + + fun setOnRequestPermissionsCallback(callback: OnRequestPermissionsResultCallback) { + sRequestCallback = callback + } + + fun setOnPermissionsResultCallback(callback: OnPermissionsResultCallback) { + sResultCallback = callback + } + + +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt b/app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt new file mode 100755 index 000000000..f54775d6d --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt @@ -0,0 +1,444 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.TypedArray +import android.graphics.Color +import android.os.Bundle +import android.util.AttributeSet +import androidx.annotation.ColorInt +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceViewHolder +import com.jaredrummler.android.colorpicker.* +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.applyTint + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ColorPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), + ColorPickerDialogListener { + + var onSaveColor: ((color: Int) -> Boolean)? = null + + private val sizeNormal = 0 + private val sizeLarge = 1 + + private var onShowDialogListener: OnShowDialogListener? = null + private var mColor = Color.BLACK + private var showDialog: Boolean = false + + @ColorPickerDialog.DialogType + private var dialogType: Int = 0 + private var colorShape: Int = 0 + private var allowPresets: Boolean = false + private var allowCustom: Boolean = false + private var showAlphaSlider: Boolean = false + private var showColorShades: Boolean = false + private var previewSize: Int = 0 + private var presets: IntArray? = null + private var dialogTitle: Int = 0 + + init { + isPersistent = true + layoutResource = io.legado.app.R.layout.view_preference + + val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference) + showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true) + + dialogType = + a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS) + colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE) + allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true) + allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true) + showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false) + showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true) + previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, sizeNormal) + val presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0) + dialogTitle = + a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title) + presets = if (presetsResId != 0) { + context.resources.getIntArray(presetsResId) + } else { + ColorPickerDialog.MATERIAL_COLORS + } + widgetLayoutResource = if (colorShape == ColorShape.CIRCLE) { + if (previewSize == sizeLarge) R.layout.cpv_preference_circle_large else R.layout.cpv_preference_circle + } else { + if (previewSize == sizeLarge) R.layout.cpv_preference_square_large else R.layout.cpv_preference_square + } + a.recycle() + } + + override fun onClick() { + super.onClick() + if (onShowDialogListener != null) { + onShowDialogListener!!.onShowColorPickerDialog(title as String, mColor) + } else if (showDialog) { + val dialog = ColorPickerDialogCompat.newBuilder() + .setDialogType(dialogType) + .setDialogTitle(dialogTitle) + .setColorShape(colorShape) + .setPresets(presets!!) + .setAllowPresets(allowPresets) + .setAllowCustom(allowCustom) + .setShowAlphaSlider(showAlphaSlider) + .setShowColorShades(showColorShades) + .setColor(mColor) + .create() + dialog.setColorPickerDialogListener(this) + getActivity().supportFragmentManager + .beginTransaction() + .add(dialog, getFragmentTag()) + .commitAllowingStateLoss() + } + } + + private fun getActivity(): FragmentActivity { + val context = context + if (context is FragmentActivity) { + return context + } else if (context is ContextWrapper) { + val baseContext = context.baseContext + if (baseContext is FragmentActivity) { + return baseContext + } + } + throw IllegalStateException("Error getting activity from context") + } + + override fun onAttached() { + super.onAttached() + if (showDialog) { + val fragment = + getActivity().supportFragmentManager.findFragmentByTag(getFragmentTag()) as ColorPickerDialog? + fragment?.setColorPickerDialogListener(this) + } + } + + override fun onBindView(holder: PreferenceViewHolder) { + val v = bindView( + context, holder, icon, title, summary, widgetLayoutResource, + R.id.cpv_preference_preview_color_panel, 30, 30 + ) + if (v is ColorPanelView) { + v.color = mColor + } + } + + override fun onSetInitialValue(defaultValue: Any?) { + super.onSetInitialValue(defaultValue) + if (defaultValue is Int) { + mColor = if (!showAlphaSlider) ColorUtils.withAlpha(defaultValue, 1f) else defaultValue + persistInt(mColor) + } else { + mColor = getPersistedInt(-0x1000000) + } + } + + override fun onGetDefaultValue(a: TypedArray, index: Int): Any { + return a.getInteger(index, Color.BLACK) + } + + override fun onColorSelected(dialogId: Int, @ColorInt color: Int) { + //返回值为true时说明已经处理过,不再处理 + if (onSaveColor?.invoke(color) == true) { + return + } + saveValue(color) + } + + override fun onDialogDismissed(dialogId: Int) { + // no-op + } + + /** + * Set the new color + * + * @param color The newly selected color + */ + fun saveValue(@ColorInt color: Int) { + mColor = if (showAlphaSlider) color else ColorUtils.withAlpha(color, 1f) + persistInt(mColor) + notifyChanged() + callChangeListener(color) + } + + /** + * Get the colors that will be shown in the [ColorPickerDialog]. + * + * @return An array of color ints + */ + fun getPresets(): IntArray? { + return presets + } + + /** + * Set the colors shown in the [ColorPickerDialog]. + * + * @param presets An array of color ints + */ + fun setPresets(presets: IntArray) { + this.presets = presets + } + + /** + * The listener used for showing the [ColorPickerDialog]. + * Call [.saveValue] after the user chooses a color. + * If this is set then it is up to you to show the dialog. + * + * @param listener The listener to show the dialog + */ + fun setOnShowDialogListener(listener: OnShowDialogListener) { + onShowDialogListener = listener + } + + /** + * The tag used for the [ColorPickerDialog]. + * + * @return The tag + */ + fun getFragmentTag(): String { + return "color_$key" + } + + interface OnShowDialogListener { + + fun onShowColorPickerDialog(title: String, currentColor: Int) + } + + + internal class ColorPickerDialogCompat : ColorPickerDialog() { + + override fun onStart() { + super.onStart() + val alertDialog = dialog as? AlertDialog + alertDialog?.applyTint() + } + + + companion object { + fun newBuilder(): Builder { + return Builder() + } + + private const val ARG_ID = "id" + private const val ARG_TYPE = "dialogType" + private const val ARG_COLOR = "color" + private const val ARG_ALPHA = "alpha" + private const val ARG_PRESETS = "presets" + private const val ARG_ALLOW_PRESETS = "allowPresets" + private const val ARG_ALLOW_CUSTOM = "allowCustom" + private const val ARG_DIALOG_TITLE = "dialogTitle" + private const val ARG_SHOW_COLOR_SHADES = "showColorShades" + private const val ARG_COLOR_SHAPE = "colorShape" + private const val ARG_PRESETS_BUTTON_TEXT = "presetsButtonText" + private const val ARG_CUSTOM_BUTTON_TEXT = "customButtonText" + private const val ARG_SELECTED_BUTTON_TEXT = "selectedButtonText" + } + + class Builder internal constructor() { + + internal var colorPickerDialogListener: ColorPickerDialogListener? = null + + @StringRes + internal var dialogTitle = R.string.cpv_default_title + + @StringRes + internal var presetsButtonText = R.string.cpv_presets + + @StringRes + internal var customButtonText = R.string.cpv_custom + + @StringRes + internal var selectedButtonText = R.string.cpv_select + + @DialogType + internal var dialogType = TYPE_PRESETS + internal var presets = MATERIAL_COLORS + + @ColorInt + internal var color = Color.BLACK + internal var dialogId = 0 + internal var showAlphaSlider = false + internal var allowPresets = true + internal var allowCustom = true + internal var showColorShades = true + + @ColorShape + internal var colorShape = ColorShape.CIRCLE + + /** + * Set the dialog title string resource id + * + * @param dialogTitle The string resource used for the dialog title + * @return This builder object for chaining method calls + */ + fun setDialogTitle(@StringRes dialogTitle: Int): Builder { + this.dialogTitle = dialogTitle + return this + } + + /** + * Set the selected button text string resource id + * + * @param selectedButtonText The string resource used for the selected button text + * @return This builder object for chaining method calls + */ + fun setSelectedButtonText(@StringRes selectedButtonText: Int): Builder { + this.selectedButtonText = selectedButtonText + return this + } + + /** + * Set the presets button text string resource id + * + * @param presetsButtonText The string resource used for the presets button text + * @return This builder object for chaining method calls + */ + fun setPresetsButtonText(@StringRes presetsButtonText: Int): Builder { + this.presetsButtonText = presetsButtonText + return this + } + + /** + * Set the custom button text string resource id + * + * @param customButtonText The string resource used for the custom button text + * @return This builder object for chaining method calls + */ + fun setCustomButtonText(@StringRes customButtonText: Int): Builder { + this.customButtonText = customButtonText + return this + } + + /** + * Set which dialog view to show. + * + * @param dialogType Either [ColorPickerDialog.TYPE_CUSTOM] or [ColorPickerDialog.TYPE_PRESETS]. + * @return This builder object for chaining method calls + */ + fun setDialogType(@DialogType dialogType: Int): Builder { + this.dialogType = dialogType + return this + } + + /** + * Set the colors used for the presets + * + * @param presets An array of color ints. + * @return This builder object for chaining method calls + */ + fun setPresets(presets: IntArray): Builder { + this.presets = presets + return this + } + + /** + * Set the original color + * + * @param color The default color for the color picker + * @return This builder object for chaining method calls + */ + fun setColor(color: Int): Builder { + this.color = color + return this + } + + /** + * Set the dialog id used for callbacks + * + * @param dialogId The id that is sent back to the [ColorPickerDialogListener]. + * @return This builder object for chaining method calls + */ + fun setDialogId(dialogId: Int): Builder { + this.dialogId = dialogId + return this + } + + /** + * Show the alpha slider + * + * @param showAlphaSlider `true` to show the alpha slider. Currently only supported with the [ ]. + * @return This builder object for chaining method calls + */ + fun setShowAlphaSlider(showAlphaSlider: Boolean): Builder { + this.showAlphaSlider = showAlphaSlider + return this + } + + /** + * Show/Hide a neutral button to select preset colors. + * + * @param allowPresets `false` to disable showing the presets button. + * @return This builder object for chaining method calls + */ + fun setAllowPresets(allowPresets: Boolean): Builder { + this.allowPresets = allowPresets + return this + } + + /** + * Show/Hide the neutral button to select a custom color. + * + * @param allowCustom `false` to disable showing the custom button. + * @return This builder object for chaining method calls + */ + fun setAllowCustom(allowCustom: Boolean): Builder { + this.allowCustom = allowCustom + return this + } + + /** + * Show/Hide the color shades in the presets picker + * + * @param showColorShades `false` to hide the color shades. + * @return This builder object for chaining method calls + */ + fun setShowColorShades(showColorShades: Boolean): Builder { + this.showColorShades = showColorShades + return this + } + + /** + * Set the shape of the color panel view. + * + * @param colorShape Either [ColorShape.CIRCLE] or [ColorShape.SQUARE]. + * @return This builder object for chaining method calls + */ + fun setColorShape(colorShape: Int): Builder { + this.colorShape = colorShape + return this + } + + /** + * Create the [ColorPickerDialog] instance. + * + * @return A new [ColorPickerDialog]. + * @see .show + */ + fun create(): ColorPickerDialog { + val dialog = + ColorPickerDialogCompat() + val args = Bundle() + args.putInt(ARG_ID, dialogId) + args.putInt(ARG_TYPE, dialogType) + args.putInt(ARG_COLOR, color) + args.putIntArray(ARG_PRESETS, presets) + args.putBoolean(ARG_ALPHA, showAlphaSlider) + args.putBoolean(ARG_ALLOW_CUSTOM, allowCustom) + args.putBoolean(ARG_ALLOW_PRESETS, allowPresets) + args.putInt(ARG_DIALOG_TITLE, dialogTitle) + args.putBoolean(ARG_SHOW_COLOR_SHADES, showColorShades) + args.putInt(ARG_COLOR_SHAPE, colorShape) + args.putInt(ARG_PRESETS_BUTTON_TEXT, presetsButtonText) + args.putInt(ARG_CUSTOM_BUTTON_TEXT, customButtonText) + args.putInt(ARG_SELECTED_BUTTON_TEXT, selectedButtonText) + dialog.arguments = args + return dialog + } + + } + + } +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/EditTextPreference.kt b/app/src/main/java/io/legado/app/lib/prefs/EditTextPreference.kt new file mode 100755 index 000000000..defe645a8 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/EditTextPreference.kt @@ -0,0 +1,36 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import androidx.preference.EditTextPreference.OnBindEditTextListener +import androidx.preference.PreferenceViewHolder +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +class EditTextPreference(context: Context, attrs: AttributeSet) : + androidx.preference.EditTextPreference(context, attrs) { + + private var mOnBindEditTextListener: OnBindEditTextListener? = null + private val onBindEditTextListener = OnBindEditTextListener { editText -> + editText.applyTint(context.accentColor) + mOnBindEditTextListener?.onBindEditText(editText) + } + + init { + // isPersistent = true + layoutResource = R.layout.view_preference + super.setOnBindEditTextListener(onBindEditTextListener) + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + Preference.bindView(context, holder, icon, title, summary, null, null) + super.onBindViewHolder(holder) + } + + override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) { + mOnBindEditTextListener = onBindEditTextListener + } + +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt b/app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt new file mode 100755 index 000000000..243ba7c5f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt @@ -0,0 +1,66 @@ +package io.legado.app.lib.prefs + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.preference.EditTextPreferenceDialogFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.filletBackground +import io.legado.app.utils.dpToPx + +class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() { + + companion object { + + fun newInstance(key: String): EditTextPreferenceDialog { + val fragment = EditTextPreferenceDialog() + val b = Bundle(1) + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key) + fragment.arguments = b + return fragment + } + + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(requireContext().filletBackground) + dialog.window?.decorView?.post { + (dialog as AlertDialog).run { + getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor) + getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor) + getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor) + } + } + return dialog + } + + override fun onStart() { + super.onStart() + if (AppConfig.isEInkMode) { + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.windowAnimations = 0 + it.attributes = attr + it.setBackgroundDrawableResource(R.color.transparent) + when (attr.gravity) { + Gravity.TOP -> it.decorView.setBackgroundResource(R.drawable.bg_eink_border_bottom) + Gravity.BOTTOM -> it.decorView.setBackgroundResource(R.drawable.bg_eink_border_top) + else -> { + val padding = 2.dpToPx(); + it.decorView.setPadding(padding, padding, padding, padding) + it.decorView.setBackgroundResource(R.drawable.bg_eink_border_dialog) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/prefs/IconListPreference.kt b/app/src/main/java/io/legado/app/lib/prefs/IconListPreference.kt new file mode 100755 index 000000000..046d6d7d6 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/IconListPreference.kt @@ -0,0 +1,215 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.content.ContextWrapper +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.fragment.app.FragmentActivity +import androidx.preference.ListPreference +import androidx.preference.PreferenceViewHolder +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemIconPreferenceBinding +import io.legado.app.lib.theme.primaryColor +import io.legado.app.utils.getCompatDrawable +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding + + +class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { + private var iconNames: Array + private val mEntryDrawables = arrayListOf() + + init { + layoutResource = R.layout.view_preference + widgetLayoutResource = R.layout.view_icon + + val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0) + + iconNames = try { + a.getTextArray(R.styleable.IconListPreference_icons) + } finally { + a.recycle() + } + + for (iconName in iconNames) { + val resId = context.resources + .getIdentifier(iconName.toString(), "mipmap", context.packageName) + var d: Drawable? = null + kotlin.runCatching { + d = context.getCompatDrawable(resId) + } + mEntryDrawables.add(d) + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val v = Preference.bindView( + context, + holder, + icon, + title, + summary, + widgetLayoutResource, + R.id.preview, + 50, + 50 + ) + if (v is ImageView) { + val selectedIndex = findIndexOfValue(value) + if (selectedIndex >= 0) { + val drawable = mEntryDrawables[selectedIndex] + v.setImageDrawable(drawable) + } + } + } + + override fun onClick() { + getActivity()?.let { + val dialog = IconDialog().apply { + val args = Bundle() + args.putString("value", value) + args.putCharSequenceArray("entries", entries) + args.putCharSequenceArray("entryValues", entryValues) + args.putCharSequenceArray("iconNames", iconNames) + arguments = args + onChanged = { value -> + this@IconListPreference.value = value + } + } + it.supportFragmentManager + .beginTransaction() + .add(dialog, getFragmentTag()) + .commitAllowingStateLoss() + } + } + + override fun onAttached() { + super.onAttached() + val fragment = + getActivity()?.supportFragmentManager?.findFragmentByTag(getFragmentTag()) as IconDialog? + fragment?.onChanged = { value -> + this@IconListPreference.value = value + } + } + + private fun getActivity(): FragmentActivity? { + val context = context + if (context is FragmentActivity) { + return context + } else if (context is ContextWrapper) { + val baseContext = context.baseContext + if (baseContext is FragmentActivity) { + return baseContext + } + } + return null + } + + private fun getFragmentTag(): String { + return "icon_$key" + } + + class IconDialog : BaseDialogFragment(R.layout.dialog_recycler_view) { + + var onChanged: ((value: String) -> Unit)? = null + var dialogValue: String? = null + var dialogEntries: Array? = null + var dialogEntryValues: Array? = null + var dialogIconNames: Array? = null + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + + override fun onStart() { + super.onStart() + setLayout(0.8f, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.change_icon) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + val adapter = Adapter(requireContext()) + binding.recyclerView.adapter = adapter + arguments?.let { + dialogValue = it.getString("value") + dialogEntries = it.getCharSequenceArray("entries") + dialogEntryValues = it.getCharSequenceArray("entryValues") + dialogIconNames = it.getCharSequenceArray("iconNames") + dialogEntryValues?.let { values -> + adapter.setItems(values.toList()) + } + } + } + + + inner class Adapter(context: Context) : + RecyclerAdapter(context) { + + override fun getViewBinding(parent: ViewGroup): ItemIconPreferenceBinding { + return ItemIconPreferenceBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemIconPreferenceBinding, + item: CharSequence, + payloads: MutableList + ) { + binding.run { + val index = findIndexOfValue(item.toString()) + dialogEntries?.let { + label.text = it[index] + } + dialogIconNames?.let { + val resId = context.resources + .getIdentifier(it[index].toString(), "mipmap", context.packageName) + val d = try { + context.getCompatDrawable(resId) + } catch (e: Exception) { + null + } + d?.let { + icon.setImageDrawable(d) + } + } + label.isChecked = item.toString() == dialogValue + root.setOnClickListener { + onChanged?.invoke(item.toString()) + this@IconDialog.dismissAllowingStateLoss() + } + } + } + + override fun registerListener( + holder: ItemViewHolder, + binding: ItemIconPreferenceBinding + ) { + holder.itemView.setOnClickListener { + getItem(holder.layoutPosition)?.let { + onChanged?.invoke(it.toString()) + } + } + } + + private fun findIndexOfValue(value: String?): Int { + dialogEntryValues?.let { values -> + for (i in values.indices.reversed()) { + if (values[i] == value) { + return i + } + } + } + return -1 + } + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt b/app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt new file mode 100755 index 000000000..deb32dc5b --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt @@ -0,0 +1,71 @@ +package io.legado.app.lib.prefs + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.core.view.forEach +import androidx.preference.ListPreferenceDialogFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.filletBackground +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx + +class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() { + + companion object { + + fun newInstance(key: String?): ListPreferenceDialog { + val fragment = ListPreferenceDialog() + val b = Bundle(1) + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key) + fragment.arguments = b + return fragment + } + + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(requireContext().filletBackground) + dialog.window?.decorView?.post { + (dialog as AlertDialog).run { + getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor) + getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor) + getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor) + listView?.forEach { + it.applyTint(accentColor) + } + } + } + return dialog + } + + override fun onStart() { + super.onStart() + if (AppConfig.isEInkMode) { + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.windowAnimations = 0 + it.attributes = attr + it.setBackgroundDrawableResource(R.color.transparent) + when (attr.gravity) { + Gravity.TOP -> it.decorView.setBackgroundResource(R.drawable.bg_eink_border_bottom) + Gravity.BOTTOM -> it.decorView.setBackgroundResource(R.drawable.bg_eink_border_top) + else -> { + val padding = 2.dpToPx(); + it.decorView.setPadding(padding, padding, padding, padding) + it.decorView.setBackgroundResource(R.drawable.bg_eink_border_dialog) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt b/app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt new file mode 100755 index 000000000..f6d59704f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt @@ -0,0 +1,73 @@ +package io.legado.app.lib.prefs + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.core.view.forEach +import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat +import androidx.preference.PreferenceDialogFragmentCompat +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.filletBackground +import io.legado.app.utils.applyTint +import io.legado.app.utils.dpToPx + +class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() { + + companion object { + + fun newInstance(key: String?): MultiSelectListPreferenceDialog { + val fragment = + MultiSelectListPreferenceDialog() + val b = Bundle(1) + b.putString(PreferenceDialogFragmentCompat.ARG_KEY, key) + fragment.arguments = b + return fragment + } + + } + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(requireContext().filletBackground) + dialog.window?.decorView?.post { + (dialog as AlertDialog).run { + getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor) + getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor) + getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor) + listView?.forEach { + it.applyTint(accentColor) + } + } + } + return dialog + } + + override fun onStart() { + super.onStart() + if (AppConfig.isEInkMode) { + dialog?.window?.let { + it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + val attr = it.attributes + attr.dimAmount = 0.0f + attr.windowAnimations = 0 + it.attributes = attr + it.setBackgroundDrawableResource(R.color.transparent) + when (attr.gravity) { + Gravity.TOP -> it.decorView.setBackgroundResource(R.drawable.bg_eink_border_bottom) + Gravity.BOTTOM -> it.decorView.setBackgroundResource(R.drawable.bg_eink_border_top) + else -> { + val padding = 2.dpToPx(); + it.decorView.setPadding(padding, padding, padding, padding) + it.decorView.setBackgroundResource(R.drawable.bg_eink_border_dialog) + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/prefs/NameListPreference.kt b/app/src/main/java/io/legado/app/lib/prefs/NameListPreference.kt new file mode 100755 index 000000000..ca1abe128 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/NameListPreference.kt @@ -0,0 +1,42 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import androidx.preference.ListPreference +import androidx.preference.PreferenceViewHolder +import io.legado.app.R +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.utils.ColorUtils + + +class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { + + private val isBottomBackground: Boolean + + init { + layoutResource = R.layout.view_preference + widgetLayoutResource = R.layout.item_fillet_text + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Preference) + isBottomBackground = typedArray.getBoolean(R.styleable.Preference_isBottomBackground, false) + typedArray.recycle() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + val v = Preference.bindView( + context, holder, icon, title, summary, widgetLayoutResource, + R.id.text_view, isBottomBackground = isBottomBackground + ) + if (v is TextView) { + v.text = entry + if (isBottomBackground) { + val bgColor = context.bottomBackground + val pTextColor = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor)) + v.setTextColor(pTextColor) + } + } + super.onBindViewHolder(holder) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/prefs/Preference.kt b/app/src/main/java/io/legado/app/lib/prefs/Preference.kt new file mode 100755 index 000000000..619b5cc98 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/Preference.kt @@ -0,0 +1,132 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.preference.PreferenceViewHolder +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.lib.theme.getSecondaryTextColor +import io.legado.app.utils.ColorUtils +import splitties.views.onLongClick +import kotlin.math.roundToInt + +open class Preference(context: Context, attrs: AttributeSet) : + androidx.preference.Preference(context, attrs) { + + private var onLongClick: ((preference: Preference) -> Boolean)? = null + private val isBottomBackground: Boolean + + init { + layoutResource = R.layout.view_preference + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Preference) + isBottomBackground = typedArray.getBoolean(R.styleable.Preference_isBottomBackground, false) + typedArray.recycle() + } + + companion object { + + fun bindView( + context: Context, + viewHolder: PreferenceViewHolder?, + icon: Drawable?, + title: CharSequence?, + summary: CharSequence?, + weightLayoutRes: Int? = null, + viewId: Int? = null, + weightWidth: Int = 0, + weightHeight: Int = 0, + isBottomBackground: Boolean = false + ): T? { + if (viewHolder == null) return null + val tvTitle = viewHolder.findViewById(R.id.preference_title) as? TextView + tvTitle?.let { + tvTitle.text = title + tvTitle.isVisible = !title.isNullOrEmpty() + } + val tvSummary = viewHolder.findViewById(R.id.preference_desc) as? TextView + tvSummary?.let { + tvSummary.text = summary + tvSummary.isGone = summary.isNullOrEmpty() + } + if (isBottomBackground && !viewHolder.itemView.isInEditMode) { + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val pTextColor = context.getPrimaryTextColor(isLight) + tvTitle?.setTextColor(pTextColor) + val sTextColor = context.getSecondaryTextColor(isLight) + tvSummary?.setTextColor(sTextColor) + } + val iconView = viewHolder.findViewById(R.id.preference_icon) + if (iconView is ImageView) { + iconView.isVisible = icon != null + iconView.setImageDrawable(icon) + iconView.setColorFilter(context.accentColor) + } + + if (weightLayoutRes != null && weightLayoutRes != 0 && viewId != null && viewId != 0) { + val lay = viewHolder.findViewById(R.id.preference_widget) + if (lay is FrameLayout) { + var needRequestLayout = false + var v = viewHolder.itemView.findViewById(viewId) + if (v == null) { + val inflater: LayoutInflater = LayoutInflater.from(context) + val childView = inflater.inflate(weightLayoutRes, null) + lay.removeAllViews() + lay.addView(childView) + lay.isVisible = true + v = lay.findViewById(viewId) + } else + needRequestLayout = true + + if (weightWidth > 0 || weightHeight > 0) { + val lp = lay.layoutParams + if (weightHeight > 0) + lp.height = + (context.resources.displayMetrics.density * weightHeight).roundToInt() + if (weightWidth > 0) + lp.width = + (context.resources.displayMetrics.density * weightWidth).roundToInt() + lay.layoutParams = lp + } else if (needRequestLayout) + v.requestLayout() + + return v + } + } + + return null + } + + } + + final override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + onBindView(holder) + onLongClick?.let { listener -> + holder.itemView.onLongClick { + listener.invoke(this) + } + } + } + + open fun onBindView(holder: PreferenceViewHolder) { + bindView( + context, holder, icon, title, summary, + isBottomBackground = isBottomBackground + ) + } + + fun onLongClick(listener: (preference: Preference) -> Boolean) { + onLongClick = listener + } + +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/PreferenceCategory.kt b/app/src/main/java/io/legado/app/lib/prefs/PreferenceCategory.kt new file mode 100755 index 000000000..725fab609 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/PreferenceCategory.kt @@ -0,0 +1,58 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceViewHolder +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.utils.ColorUtils + + +class PreferenceCategory(context: Context, attrs: AttributeSet) : + PreferenceCategory(context, attrs) { + + init { + isPersistent = true + layoutResource = R.layout.view_preference_category + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val view = holder.findViewById(R.id.preference_title) + if (view is TextView) { // && !view.isInEditMode + view.text = title + if (view.isInEditMode) return + view.setTextColor(context.accentColor) + view.isVisible = !title.isNullOrEmpty() + + val da = holder.findViewById(R.id.preference_divider_above) + val dividerColor = if (AppConfig.isNightTheme) { + ColorUtils.withAlpha( + ColorUtils.shiftColor(context.backgroundColor, 1.05f), + 0.5f + ) + } else { + ColorUtils.withAlpha( + ColorUtils.shiftColor(context.backgroundColor, 0.95f), + 0.5f + ) + } + if (da is View) { + da.setBackgroundColor(dividerColor) + da.isVisible = holder.isDividerAllowedAbove + } + val db = holder.findViewById(R.id.preference_divider_below) + if (db is View) { + db.setBackgroundColor(dividerColor) + db.isVisible = holder.isDividerAllowedBelow + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt b/app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt new file mode 100755 index 000000000..cf6f82063 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt @@ -0,0 +1,47 @@ +package io.legado.app.lib.prefs + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.SwitchCompat +import androidx.preference.PreferenceViewHolder +import androidx.preference.SwitchPreferenceCompat +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +class SwitchPreference(context: Context, attrs: AttributeSet) : + SwitchPreferenceCompat(context, attrs) { + + private val isBottomBackground: Boolean + private var onLongClick: ((preference: SwitchPreference) -> Boolean)? = null + + init { + layoutResource = R.layout.view_preference + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Preference) + isBottomBackground = typedArray.getBoolean(R.styleable.Preference_isBottomBackground, false) + typedArray.recycle() + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + val v = Preference.bindView( + context, holder, icon, title, summary, + widgetLayoutResource, + androidx.preference.R.id.switchWidget, + isBottomBackground = isBottomBackground + ) + if (v is SwitchCompat && !v.isInEditMode) { + v.applyTint(context.accentColor) + } + super.onBindViewHolder(holder) + onLongClick?.let { listener -> + holder.itemView.setOnLongClickListener { + listener.invoke(this) + } + } + } + + fun onLongClick(listener: (preference: SwitchPreference) -> Boolean) { + onLongClick = listener + } + +} diff --git a/app/src/main/java/io/legado/app/lib/prefs/fragment/PreferenceFragment.kt b/app/src/main/java/io/legado/app/lib/prefs/fragment/PreferenceFragment.kt new file mode 100755 index 000000000..e5fef1006 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/prefs/fragment/PreferenceFragment.kt @@ -0,0 +1,75 @@ +package io.legado.app.lib.prefs.fragment + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.fragment.app.DialogFragment +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import io.legado.app.lib.prefs.EditTextPreferenceDialog +import io.legado.app.lib.prefs.ListPreferenceDialog +import io.legado.app.lib.prefs.MultiSelectListPreferenceDialog +import io.legado.app.utils.applyNavigationBarPadding + +abstract class PreferenceFragment : PreferenceFragmentCompat() { + + private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG" + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listView.clipToPadding = false + listView.applyNavigationBarPadding() + } + + @SuppressLint("RestrictedApi") + override fun onDisplayPreferenceDialog(preference: Preference) { + + var handled = false + if (callbackFragment is OnPreferenceDisplayDialogCallback) { + handled = + (callbackFragment as OnPreferenceDisplayDialogCallback) + .onPreferenceDisplayDialog(this, preference) + } + if (!handled && activity is OnPreferenceDisplayDialogCallback) { + handled = (activity as OnPreferenceDisplayDialogCallback) + .onPreferenceDisplayDialog(this, preference) + } + + if (handled) { + return + } + + // check if dialog is already showing + if (parentFragmentManager.findFragmentByTag(dialogFragmentTag) != null) { + return + } + + val dialogFragment: DialogFragment = when (preference) { + is EditTextPreference -> { + EditTextPreferenceDialog.newInstance(preference.getKey()) + } + is ListPreference -> { + ListPreferenceDialog.newInstance(preference.getKey()) + } + is MultiSelectListPreference -> { + MultiSelectListPreferenceDialog.newInstance(preference.getKey()) + } + else -> { + throw IllegalArgumentException( + "Cannot display dialog for an unknown Preference type: " + + preference.javaClass.simpleName + + ". Make sure to implement onPreferenceDisplayDialog() to handle " + + "displaying a custom dialog for this Preference." + ) + } + } + @Suppress("DEPRECATION") + dialogFragment.setTargetFragment(this, 0) + + dialogFragment.show(parentFragmentManager, dialogFragmentTag) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt b/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt new file mode 100755 index 000000000..de0e72550 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt @@ -0,0 +1,148 @@ +@file:Suppress("unused") + +package io.legado.app.lib.theme + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.GradientDrawable +import androidx.annotation.ColorInt +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import io.legado.app.R +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dpToPx + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@ColorInt +fun Context.getPrimaryTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.md_light_primary_text) + } else { + ContextCompat.getColor(this, R.color.md_dark_primary_text) + } +} + +@ColorInt +fun Context.getSecondaryTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.md_light_secondary) + } else { + ContextCompat.getColor(this, R.color.md_dark_primary_text) + } +} + +@ColorInt +fun Context.getPrimaryDisabledTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor(this, R.color.md_light_disabled) + } else { + ContextCompat.getColor(this, R.color.md_dark_disabled) + } +} + +@ColorInt +fun Context.getSecondaryDisabledTextColor(dark: Boolean): Int { + return if (dark) { + ContextCompat.getColor( + this, + androidx.appcompat.R.color.secondary_text_disabled_material_light + ) + } else { + ContextCompat.getColor( + this, + androidx.appcompat.R.color.secondary_text_disabled_material_dark + ) + } +} + +val Context.primaryColor: Int + get() = ThemeStore.primaryColor(this) + +val Context.primaryColorDark: Int + get() = ThemeStore.primaryColorDark(this) + +val Context.accentColor: Int + get() = ThemeStore.accentColor(this) + +val Context.backgroundColor: Int + get() = ThemeStore.backgroundColor(this) + +val Context.bottomBackground: Int + get() = ThemeStore.bottomBackground(this) + +val Context.primaryTextColor: Int + get() = getPrimaryTextColor(isDarkTheme) + +val Context.secondaryTextColor: Int + get() = getSecondaryTextColor(isDarkTheme) + +val Context.primaryDisabledTextColor: Int + get() = getPrimaryDisabledTextColor(isDarkTheme) + +val Context.secondaryDisabledTextColor: Int + get() = getSecondaryDisabledTextColor(isDarkTheme) + +val Fragment.primaryColor: Int + get() = ThemeStore.primaryColor(requireContext()) + +val Fragment.primaryColorDark: Int + get() = ThemeStore.primaryColorDark(requireContext()) + +val Fragment.accentColor: Int + get() = ThemeStore.accentColor(requireContext()) + +val Fragment.backgroundColor: Int + get() = ThemeStore.backgroundColor(requireContext()) + +val Fragment.bottomBackground: Int + get() = ThemeStore.bottomBackground(requireContext()) + +val Fragment.primaryTextColor: Int + get() = requireContext().getPrimaryTextColor(isDarkTheme) + +val Fragment.secondaryTextColor: Int + get() = requireContext().getSecondaryTextColor(isDarkTheme) + +val Fragment.primaryDisabledTextColor: Int + get() = requireContext().getPrimaryDisabledTextColor(isDarkTheme) + +val Fragment.secondaryDisabledTextColor: Int + get() = requireContext().getSecondaryDisabledTextColor(isDarkTheme) + +val Context.buttonDisabledColor: Int + get() = if (isDarkTheme) { + ContextCompat.getColor(this, R.color.md_dark_disabled) + } else { + ContextCompat.getColor(this, R.color.md_light_disabled) + } + +val Context.isDarkTheme: Boolean + get() = ColorUtils.isColorLight(ThemeStore.primaryColor(this)) + +val Fragment.isDarkTheme: Boolean + get() = requireContext().isDarkTheme + +val Context.elevation: Float + @SuppressLint("PrivateResource") + get() { + return if (AppConfig.elevation < 0) { + ThemeUtils.resolveFloat( + this, + android.R.attr.elevation, + resources.getDimension(com.google.android.material.R.dimen.design_appbar_elevation) + ) + } else { + AppConfig.elevation.toFloat().dpToPx() + } + } + +val Context.filletBackground: GradientDrawable + get() { + val background = GradientDrawable() + background.cornerRadius = 3f.dpToPx() + background.setColor(backgroundColor) + return background + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/Selector.kt b/app/src/main/java/io/legado/app/lib/theme/Selector.kt new file mode 100755 index 000000000..f9985528f --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/Selector.kt @@ -0,0 +1,450 @@ +package io.legado.app.lib.theme + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.StateListDrawable +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import androidx.annotation.DrawableRes +import androidx.annotation.IntDef +import androidx.core.content.ContextCompat + +@Suppress("unused") +object Selector { + fun shapeBuild(): ShapeSelector { + return ShapeSelector() + } + + fun colorBuild(): ColorSelector { + return ColorSelector() + } + + fun drawableBuild(): DrawableSelector { + return DrawableSelector() + } + + /** + * 形状ShapeSelector + * + * @author hjy + * created at 2017/12/11 22:26 + */ + class ShapeSelector { + + private var mShape: Int = 0 //the shape of background + private var mDefaultBgColor: Int = 0 //default background color + private var mDisabledBgColor: Int = 0 //state_enabled = false + private var mPressedBgColor: Int = 0 //state_pressed = true + private var mSelectedBgColor: Int = 0 //state_selected = true + private var mFocusedBgColor: Int = 0 //state_focused = true + private var mCheckedBgColor: Int = 0 //state_checked = true + private var mStrokeWidth: Int = 0 //stroke width in pixel + private var mDefaultStrokeColor: Int = 0 //default stroke color + private var mDisabledStrokeColor: Int = 0 //state_enabled = false + private var mPressedStrokeColor: Int = 0 //state_pressed = true + private var mSelectedStrokeColor: Int = 0 //state_selected = true + private var mFocusedStrokeColor: Int = 0 //state_focused = true + private var mCheckedStrokeColor: Int = 0 //state_checked = true + private var mCornerRadius: Int = 0 //corner radius + + private var hasSetDisabledBgColor = false + private var hasSetPressedBgColor = false + private var hasSetSelectedBgColor = false + private val hasSetFocusedBgColor = false + private var hasSetCheckedBgColor = false + + private var hasSetDisabledStrokeColor = false + private var hasSetPressedStrokeColor = false + private var hasSetSelectedStrokeColor = false + private var hasSetFocusedStrokeColor = false + private var hasSetCheckedStrokeColor = false + + @IntDef( + GradientDrawable.RECTANGLE, + GradientDrawable.OVAL, + GradientDrawable.LINE, + GradientDrawable.RING + ) + private annotation class Shape + + init { + //initialize default values + mShape = GradientDrawable.RECTANGLE + mDefaultBgColor = Color.TRANSPARENT + mDisabledBgColor = Color.TRANSPARENT + mPressedBgColor = Color.TRANSPARENT + mSelectedBgColor = Color.TRANSPARENT + mFocusedBgColor = Color.TRANSPARENT + mStrokeWidth = 0 + mDefaultStrokeColor = Color.TRANSPARENT + mDisabledStrokeColor = Color.TRANSPARENT + mPressedStrokeColor = Color.TRANSPARENT + mSelectedStrokeColor = Color.TRANSPARENT + mFocusedStrokeColor = Color.TRANSPARENT + mCornerRadius = 0 + } + + fun setShape(@Shape shape: Int): ShapeSelector { + mShape = shape + return this + } + + fun setDefaultBgColor(@ColorInt color: Int): ShapeSelector { + mDefaultBgColor = color + if (!hasSetDisabledBgColor) + mDisabledBgColor = color + if (!hasSetPressedBgColor) + mPressedBgColor = color + if (!hasSetSelectedBgColor) + mSelectedBgColor = color + if (!hasSetFocusedBgColor) + mFocusedBgColor = color + return this + } + + fun setDisabledBgColor(@ColorInt color: Int): ShapeSelector { + mDisabledBgColor = color + hasSetDisabledBgColor = true + return this + } + + fun setPressedBgColor(@ColorInt color: Int): ShapeSelector { + mPressedBgColor = color + hasSetPressedBgColor = true + return this + } + + fun setSelectedBgColor(@ColorInt color: Int): ShapeSelector { + mSelectedBgColor = color + hasSetSelectedBgColor = true + return this + } + + fun setFocusedBgColor(@ColorInt color: Int): ShapeSelector { + mFocusedBgColor = color + hasSetPressedBgColor = true + return this + } + + fun setCheckedBgColor(@ColorInt color: Int): ShapeSelector { + mCheckedBgColor = color + hasSetCheckedBgColor = true + return this + } + + fun setStrokeWidth(@Dimension width: Int): ShapeSelector { + mStrokeWidth = width + return this + } + + fun setDefaultStrokeColor(@ColorInt color: Int): ShapeSelector { + mDefaultStrokeColor = color + if (!hasSetDisabledStrokeColor) + mDisabledStrokeColor = color + if (!hasSetPressedStrokeColor) + mPressedStrokeColor = color + if (!hasSetSelectedStrokeColor) + mSelectedStrokeColor = color + if (!hasSetFocusedStrokeColor) + mFocusedStrokeColor = color + return this + } + + fun setDisabledStrokeColor(@ColorInt color: Int): ShapeSelector { + mDisabledStrokeColor = color + hasSetDisabledStrokeColor = true + return this + } + + fun setPressedStrokeColor(@ColorInt color: Int): ShapeSelector { + mPressedStrokeColor = color + hasSetPressedStrokeColor = true + return this + } + + fun setSelectedStrokeColor(@ColorInt color: Int): ShapeSelector { + mSelectedStrokeColor = color + hasSetSelectedStrokeColor = true + return this + } + + fun setCheckedStrokeColor(@ColorInt color: Int): ShapeSelector { + mCheckedStrokeColor = color + hasSetCheckedStrokeColor = true + return this + } + + fun setFocusedStrokeColor(@ColorInt color: Int): ShapeSelector { + mFocusedStrokeColor = color + hasSetFocusedStrokeColor = true + return this + } + + fun setCornerRadius(@Dimension radius: Int): ShapeSelector { + mCornerRadius = radius + return this + } + + fun create(): StateListDrawable { + val selector = StateListDrawable() + + //enabled = false + if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) { + val disabledShape = getItemShape( + mShape, mCornerRadius, + mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor + ) + selector.addState(intArrayOf(-android.R.attr.state_enabled), disabledShape) + } + + //pressed = true + if (hasSetPressedBgColor || hasSetPressedStrokeColor) { + val pressedShape = getItemShape( + mShape, mCornerRadius, + mPressedBgColor, mStrokeWidth, mPressedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_pressed), pressedShape) + } + + //selected = true + if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) { + val selectedShape = getItemShape( + mShape, mCornerRadius, + mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_selected), selectedShape) + } + + //focused = true + if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) { + val focusedShape = getItemShape( + mShape, mCornerRadius, + mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_focused), focusedShape) + } + + //checked = true + if (hasSetCheckedBgColor || hasSetCheckedStrokeColor) { + val checkedShape = getItemShape( + mShape, mCornerRadius, + mCheckedBgColor, mStrokeWidth, mCheckedStrokeColor + ) + selector.addState(intArrayOf(android.R.attr.state_checked), checkedShape) + } + + //default + val defaultShape = getItemShape( + mShape, mCornerRadius, + mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor + ) + selector.addState(intArrayOf(), defaultShape) + + return selector + } + + private fun getItemShape( + shape: Int, cornerRadius: Int, + solidColor: Int, strokeWidth: Int, strokeColor: Int + ): GradientDrawable { + val drawable = GradientDrawable() + drawable.shape = shape + drawable.setStroke(strokeWidth, strokeColor) + drawable.cornerRadius = cornerRadius.toFloat() + drawable.setColor(solidColor) + return drawable + } + } + + /** + * 资源DrawableSelector + * + * @author hjy + * created at 2017/12/11 22:34 + */ + @Suppress("MemberVisibilityCanBePrivate") + class DrawableSelector { + + private var mDefaultDrawable: Drawable? = null + private var mDisabledDrawable: Drawable? = null + private var mPressedDrawable: Drawable? = null + private var mSelectedDrawable: Drawable? = null + private var mFocusedDrawable: Drawable? = null + + private var hasSetDisabledDrawable = false + private var hasSetPressedDrawable = false + private var hasSetSelectedDrawable = false + private var hasSetFocusedDrawable = false + + init { + mDefaultDrawable = ColorDrawable(Color.TRANSPARENT) + } + + fun setDefaultDrawable(drawable: Drawable?): DrawableSelector { + mDefaultDrawable = drawable + if (!hasSetDisabledDrawable) + mDisabledDrawable = drawable + if (!hasSetPressedDrawable) + mPressedDrawable = drawable + if (!hasSetSelectedDrawable) + mSelectedDrawable = drawable + if (!hasSetFocusedDrawable) + mFocusedDrawable = drawable + return this + } + + fun setDisabledDrawable(drawable: Drawable?): DrawableSelector { + mDisabledDrawable = drawable + hasSetDisabledDrawable = true + return this + } + + fun setPressedDrawable(drawable: Drawable?): DrawableSelector { + mPressedDrawable = drawable + hasSetPressedDrawable = true + return this + } + + fun setSelectedDrawable(drawable: Drawable?): DrawableSelector { + mSelectedDrawable = drawable + hasSetSelectedDrawable = true + return this + } + + fun setFocusedDrawable(drawable: Drawable?): DrawableSelector { + mFocusedDrawable = drawable + hasSetFocusedDrawable = true + return this + } + + fun create(): StateListDrawable { + val selector = StateListDrawable() + if (hasSetDisabledDrawable) + selector.addState(intArrayOf(-android.R.attr.state_enabled), mDisabledDrawable) + if (hasSetPressedDrawable) + selector.addState(intArrayOf(android.R.attr.state_pressed), mPressedDrawable) + if (hasSetSelectedDrawable) + selector.addState(intArrayOf(android.R.attr.state_selected), mSelectedDrawable) + if (hasSetFocusedDrawable) + selector.addState(intArrayOf(android.R.attr.state_focused), mFocusedDrawable) + selector.addState(intArrayOf(), mDefaultDrawable) + return selector + } + + fun setDefaultDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setDefaultDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setDisabledDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setDisabledDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setPressedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setPressedDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setSelectedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setSelectedDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + + fun setFocusedDrawable(context: Context, @DrawableRes drawableRes: Int): DrawableSelector { + return setFocusedDrawable(ContextCompat.getDrawable(context, drawableRes)) + } + } + + /** + * 颜色ColorSelector + * + * @author hjy + * created at 2017/12/11 22:26 + */ + class ColorSelector { + + private var mDefaultColor: Int = 0 + private var mDisabledColor: Int = 0 + private var mPressedColor: Int = 0 + private var mSelectedColor: Int = 0 + private var mFocusedColor: Int = 0 + private var mCheckedColor: Int = 0 + + private var hasSetDisabledColor = false + private var hasSetPressedColor = false + private var hasSetSelectedColor = false + private var hasSetFocusedColor = false + private var hasSetCheckedColor = false + + init { + mDefaultColor = Color.BLACK + mDisabledColor = Color.GRAY + mPressedColor = Color.BLACK + mSelectedColor = Color.BLACK + mFocusedColor = Color.BLACK + } + + fun setDefaultColor(@ColorInt color: Int): ColorSelector { + mDefaultColor = color + if (!hasSetDisabledColor) + mDisabledColor = color + if (!hasSetPressedColor) + mPressedColor = color + if (!hasSetSelectedColor) + mSelectedColor = color + if (!hasSetFocusedColor) + mFocusedColor = color + return this + } + + fun setDisabledColor(@ColorInt color: Int): ColorSelector { + mDisabledColor = color + hasSetDisabledColor = true + return this + } + + fun setPressedColor(@ColorInt color: Int): ColorSelector { + mPressedColor = color + hasSetPressedColor = true + return this + } + + fun setSelectedColor(@ColorInt color: Int): ColorSelector { + mSelectedColor = color + hasSetSelectedColor = true + return this + } + + fun setFocusedColor(@ColorInt color: Int): ColorSelector { + mFocusedColor = color + hasSetFocusedColor = true + return this + } + + fun setCheckedColor(@ColorInt color: Int): ColorSelector { + mCheckedColor = color + hasSetCheckedColor = true + return this + } + + fun create(): ColorStateList { + val colors = intArrayOf( + if (hasSetDisabledColor) mDisabledColor else mDefaultColor, + if (hasSetPressedColor) mPressedColor else mDefaultColor, + if (hasSetSelectedColor) mSelectedColor else mDefaultColor, + if (hasSetFocusedColor) mFocusedColor else mDefaultColor, + if (hasSetCheckedColor) mCheckedColor else mDefaultColor, + mDefaultColor + ) + val states = arrayOfNulls(6) + states[0] = intArrayOf(-android.R.attr.state_enabled) + states[1] = intArrayOf(android.R.attr.state_pressed) + states[2] = intArrayOf(android.R.attr.state_selected) + states[3] = intArrayOf(android.R.attr.state_focused) + states[4] = intArrayOf(android.R.attr.state_checked) + states[5] = intArrayOf() + return ColorStateList(states, colors) + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt new file mode 100755 index 000000000..ac5bb20ef --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt @@ -0,0 +1,342 @@ +package io.legado.app.lib.theme + +import android.annotation.SuppressLint +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Color +import androidx.annotation.AttrRes +import androidx.annotation.CheckResult +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.LogUtils +import splitties.init.appCtx + +/** + * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused") +class ThemeStore @SuppressLint("CommitPrefEdits") +private constructor(private val mContext: Context) : ThemeStoreInterface { + + private val mEditor = prefs(mContext).edit() + + override fun primaryColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR, color) + if (autoGeneratePrimaryDark(mContext)) + primaryColorDark(ColorUtils.darkenColor(color)) + return this + } + + override fun primaryColorRes(@ColorRes colorRes: Int): ThemeStore { + return primaryColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun primaryColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return primaryColor(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun primaryColorDark(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK, color) + return this + } + + override fun primaryColorDarkRes(@ColorRes colorRes: Int): ThemeStore { + return primaryColorDark(ContextCompat.getColor(mContext, colorRes)) + } + + override fun primaryColorDarkAttr(@AttrRes colorAttr: Int): ThemeStore { + return primaryColorDark(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun accentColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_ACCENT_COLOR, color) + return this + } + + override fun accentColorRes(@ColorRes colorRes: Int): ThemeStore { + return accentColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun accentColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return accentColor(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun statusBarColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, color) + return this + } + + override fun statusBarColorRes(@ColorRes colorRes: Int): ThemeStore { + return statusBarColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun statusBarColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return statusBarColor(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun navigationBarColor(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, color) + return this + } + + override fun navigationBarColorRes(@ColorRes colorRes: Int): ThemeStore { + return navigationBarColor(ContextCompat.getColor(mContext, colorRes)) + } + + override fun navigationBarColorAttr(@AttrRes colorAttr: Int): ThemeStore { + return navigationBarColor(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorPrimary(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY, color) + return this + } + + override fun textColorPrimaryRes(@ColorRes colorRes: Int): ThemeStore { + return textColorPrimary(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorPrimaryAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorPrimary(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorPrimaryInverse(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY_INVERSE, color) + return this + } + + override fun textColorPrimaryInverseRes(@ColorRes colorRes: Int): ThemeStore { + return textColorPrimaryInverse(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorPrimaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorPrimaryInverse(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorSecondary(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY, color) + return this + } + + override fun textColorSecondaryRes(@ColorRes colorRes: Int): ThemeStore { + return textColorSecondary(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorSecondaryAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorSecondary(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun textColorSecondaryInverse(@ColorInt color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY_INVERSE, color) + return this + } + + override fun textColorSecondaryInverseRes(@ColorRes colorRes: Int): ThemeStore { + return textColorSecondaryInverse(ContextCompat.getColor(mContext, colorRes)) + } + + override fun textColorSecondaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore { + return textColorSecondaryInverse(ThemeUtils.resolveColor(mContext, colorAttr)) + } + + override fun backgroundColor(color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, color) + return this + } + + override fun bottomBackground(color: Int): ThemeStore { + mEditor.putInt(ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, color) + return this + } + + override fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore { + mEditor.putBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate) + return this + } + + // Commit method + + override fun apply() { + mEditor.putLong(ThemeStorePrefKeys.VALUES_CHANGED, System.currentTimeMillis()) + .putBoolean(ThemeStorePrefKeys.IS_CONFIGURED_KEY, true) + .apply() + accentColor = accentColor() + } + + companion object { + + var accentColor = accentColor() + + fun editTheme(context: Context): ThemeStore { + return ThemeStore(context) + } + + // Static getters + + @CheckResult + internal fun prefs(context: Context): SharedPreferences { + return context.getSharedPreferences( + ThemeStorePrefKeys.CONFIG_PREFS_KEY_DEFAULT, + Context.MODE_PRIVATE + ) + } + + fun markChanged(context: Context) { + ThemeStore(context).apply() + } + + @CheckResult + @ColorInt + fun primaryColor(context: Context = appCtx): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_PRIMARY_COLOR, + ThemeUtils.resolveColor( + context, + androidx.appcompat.R.attr.colorPrimary, + Color.parseColor("#455A64") + ) + ) + } + + @CheckResult + @ColorInt + fun primaryColorDark(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK, + ThemeUtils.resolveColor( + context, + androidx.appcompat.R.attr.colorPrimaryDark, + Color.parseColor("#37474F") + ) + ) + } + + @CheckResult + @ColorInt + fun accentColor(context: Context = appCtx): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_ACCENT_COLOR, + ThemeUtils.resolveColor( + context, + androidx.appcompat.R.attr.colorAccent, + Color.parseColor("#263238") + ) + ) + } + + @CheckResult + @ColorInt + fun statusBarColor(context: Context, transparent: Boolean): Int { + return if (transparent) { + prefs(context).getInt( + ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, + primaryColor(context) + ) + } else { + prefs(context).getInt( + ThemeStorePrefKeys.KEY_STATUS_BAR_COLOR, + primaryColorDark(context) + ) + } + } + + @CheckResult + @ColorInt + fun navigationBarColor(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_NAVIGATION_BAR_COLOR, + bottomBackground(context) + ) + } + + @CheckResult + @ColorInt + fun textColorPrimary(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY, + ThemeUtils.resolveColor(context, android.R.attr.textColorPrimary) + ) + } + + @CheckResult + @ColorInt + fun textColorPrimaryInverse(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_PRIMARY_INVERSE, + ThemeUtils.resolveColor(context, android.R.attr.textColorPrimaryInverse) + ) + } + + @CheckResult + @ColorInt + fun textColorSecondary(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY, + ThemeUtils.resolveColor(context, android.R.attr.textColorSecondary) + ) + } + + @CheckResult + @ColorInt + fun textColorSecondaryInverse(context: Context): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_TEXT_COLOR_SECONDARY_INVERSE, + ThemeUtils.resolveColor(context, android.R.attr.textColorSecondaryInverse) + ) + } + + @CheckResult + @ColorInt + fun backgroundColor(context: Context = appCtx): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_BACKGROUND_COLOR, + ThemeUtils.resolveColor(context, android.R.attr.colorBackground) + ) + } + + @CheckResult + @ColorInt + fun bottomBackground(context: Context = appCtx): Int { + return prefs(context).getInt( + ThemeStorePrefKeys.KEY_BOTTOM_BACKGROUND, + ThemeUtils.resolveColor(context, android.R.attr.colorBackground) + ) + } + + @CheckResult + fun coloredStatusBar(context: Context): Boolean { + return prefs(context).getBoolean( + ThemeStorePrefKeys.KEY_APPLY_PRIMARYDARK_STATUSBAR, + true + ) + } + + @CheckResult + fun coloredNavigationBar(context: Context): Boolean { + return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_APPLY_PRIMARY_NAVBAR, false) + } + + @CheckResult + fun autoGeneratePrimaryDark(context: Context): Boolean { + return prefs(context).getBoolean(ThemeStorePrefKeys.KEY_AUTO_GENERATE_PRIMARYDARK, true) + } + + @CheckResult + fun isConfigured(context: Context): Boolean { + return prefs(context).getBoolean(ThemeStorePrefKeys.IS_CONFIGURED_KEY, false) + } + + @SuppressLint("CommitPrefEdits") + fun isConfigured(context: Context, version: Int): Boolean { + val prefs = prefs(context) + val lastVersion = prefs.getInt(ThemeStorePrefKeys.IS_CONFIGURED_VERSION_KEY, -1) + if (version > lastVersion) { + prefs.edit().putInt(ThemeStorePrefKeys.IS_CONFIGURED_VERSION_KEY, version).apply() + return false + } + return true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt new file mode 100755 index 000000000..f7b3ae500 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStoreInterface.kt @@ -0,0 +1,90 @@ +package io.legado.app.lib.theme + + +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes + +/** + * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) + */ +internal interface ThemeStoreInterface { + + // Primary colors + + fun primaryColor(@ColorInt color: Int): ThemeStore + + fun primaryColorRes(@ColorRes colorRes: Int): ThemeStore + + fun primaryColorAttr(@AttrRes colorAttr: Int): ThemeStore + + fun autoGeneratePrimaryDark(autoGenerate: Boolean): ThemeStore + + fun primaryColorDark(@ColorInt color: Int): ThemeStore + + fun primaryColorDarkRes(@ColorRes colorRes: Int): ThemeStore + + fun primaryColorDarkAttr(@AttrRes colorAttr: Int): ThemeStore + + // Accent colors + + fun accentColor(@ColorInt color: Int): ThemeStore + + fun accentColorRes(@ColorRes colorRes: Int): ThemeStore + + fun accentColorAttr(@AttrRes colorAttr: Int): ThemeStore + + // Status bar color + + fun statusBarColor(@ColorInt color: Int): ThemeStore + + fun statusBarColorRes(@ColorRes colorRes: Int): ThemeStore + + fun statusBarColorAttr(@AttrRes colorAttr: Int): ThemeStore + + // Navigation bar color + + fun navigationBarColor(@ColorInt color: Int): ThemeStore + + fun navigationBarColorRes(@ColorRes colorRes: Int): ThemeStore + + fun navigationBarColorAttr(@AttrRes colorAttr: Int): ThemeStore + + // Primary text color + + fun textColorPrimary(@ColorInt color: Int): ThemeStore + + fun textColorPrimaryRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorPrimaryAttr(@AttrRes colorAttr: Int): ThemeStore + + fun textColorPrimaryInverse(@ColorInt color: Int): ThemeStore + + fun textColorPrimaryInverseRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorPrimaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore + + // Secondary text color + + fun textColorSecondary(@ColorInt color: Int): ThemeStore + + fun textColorSecondaryRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorSecondaryAttr(@AttrRes colorAttr: Int): ThemeStore + + fun textColorSecondaryInverse(@ColorInt color: Int): ThemeStore + + fun textColorSecondaryInverseRes(@ColorRes colorRes: Int): ThemeStore + + fun textColorSecondaryInverseAttr(@AttrRes colorAttr: Int): ThemeStore + + // Background + + fun backgroundColor(@ColorInt color: Int): ThemeStore + + fun bottomBackground(@ColorInt color: Int): ThemeStore + + // Commit/apply + + fun apply() +} diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt new file mode 100755 index 000000000..da1e0a932 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeStorePrefKeys.kt @@ -0,0 +1,30 @@ +package io.legado.app.lib.theme + +/** + * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid) + */ +object ThemeStorePrefKeys { + + const val CONFIG_PREFS_KEY_DEFAULT = "app_themes" + const val IS_CONFIGURED_KEY = "is_configured" + const val IS_CONFIGURED_VERSION_KEY = "is_configured_version" + const val VALUES_CHANGED = "values_changed" + + const val KEY_PRIMARY_COLOR = "primary_color" + const val KEY_PRIMARY_COLOR_DARK = "primary_color_dark" + const val KEY_ACCENT_COLOR = "accent_color" + const val KEY_STATUS_BAR_COLOR = "status_bar_color" + const val KEY_NAVIGATION_BAR_COLOR = "navigation_bar_color" + + const val KEY_TEXT_COLOR_PRIMARY = "text_color_primary" + const val KEY_TEXT_COLOR_PRIMARY_INVERSE = "text_color_primary_inverse" + const val KEY_TEXT_COLOR_SECONDARY = "text_color_secondary" + const val KEY_TEXT_COLOR_SECONDARY_INVERSE = "text_color_secondary_inverse" + + const val KEY_BACKGROUND_COLOR = "backgroundColor" + const val KEY_BOTTOM_BACKGROUND = "bottomBackground" + + const val KEY_APPLY_PRIMARYDARK_STATUSBAR = "apply_primarydark_statusbar" + const val KEY_APPLY_PRIMARY_NAVBAR = "apply_primary_navbar" + const val KEY_AUTO_GENERATE_PRIMARYDARK = "auto_generate_primarydark" +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/ThemeUtils.kt b/app/src/main/java/io/legado/app/lib/theme/ThemeUtils.kt new file mode 100755 index 000000000..4599942f1 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/ThemeUtils.kt @@ -0,0 +1,44 @@ +package io.legado.app.lib.theme + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.annotation.AttrRes + +/** + * @author Aidan Follestad (afollestad) + */ +object ThemeUtils { + + @JvmOverloads + fun resolveColor(context: Context, @AttrRes attr: Int, fallback: Int = 0): Int { + val a = context.theme.obtainStyledAttributes(intArrayOf(attr)) + return try { + a.getColor(0, fallback) + } catch (e: Exception) { + fallback + } finally { + a.recycle() + } + } + + @JvmOverloads + fun resolveFloat(context: Context, @AttrRes attr: Int, fallback: Float = 0.0f): Float { + val a = context.theme.obtainStyledAttributes(intArrayOf(attr)) + return try { + a.getFloat(0, fallback) + } catch (e: Exception) { + fallback + } finally { + a.recycle() + } + } + + fun resolveDrawable(context: Context, @AttrRes attr: Int): Drawable? { + val a = context.theme.obtainStyledAttributes(intArrayOf(attr)) + return try { + a.getDrawable(0) + } finally { + a.recycle() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt b/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt new file mode 100755 index 000000000..b9f5be5c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt @@ -0,0 +1,487 @@ +package io.legado.app.lib.theme + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.graphics.drawable.RippleDrawable +import android.view.View +import android.widget.Button +import android.widget.CheckBox +import android.widget.CheckedTextView +import android.widget.EditText +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.RadioButton +import android.widget.SeekBar +import android.widget.Switch +import android.widget.TextView +import androidx.annotation.CheckResult +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.widget.TextViewCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import io.legado.app.R +import io.legado.app.utils.ColorUtils + +/** + * @author afollestad, plusCubed + */ +@Suppress("MemberVisibilityCanBePrivate") +object TintHelper { + + @SuppressLint("PrivateResource") + @ColorInt + private fun getDefaultRippleColor(context: Context, useDarkRipple: Boolean): Int { + // Light ripple is actually translucent black, and vice versa + return ContextCompat.getColor( + context, if (useDarkRipple) + androidx.appcompat.R.color.ripple_material_light + else + androidx.appcompat.R.color.ripple_material_dark + ) + } + + private fun getDisabledColorStateList( + @ColorInt normal: Int, + @ColorInt disabled: Int + ): ColorStateList { + return ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled) + ), intArrayOf(disabled, normal) + ) + } + + fun setTintSelector(view: View, @ColorInt color: Int, darker: Boolean, useDarkTheme: Boolean) { + val isColorLight = ColorUtils.isColorLight(color) + val disabled = ContextCompat.getColor( + view.context, + if (useDarkTheme) R.color.ate_button_disabled_dark else R.color.ate_button_disabled_light + ) + val pressed = ColorUtils.shiftColor(color, if (darker) 0.9f else 1.1f) + val activated = ColorUtils.shiftColor(color, if (darker) 1.1f else 0.9f) + val rippleColor = getDefaultRippleColor(view.context, isColorLight) + val textColor = ContextCompat.getColor( + view.context, + if (isColorLight) R.color.ate_primary_text_light else R.color.ate_primary_text_dark + ) + + val sl: ColorStateList + when (view) { + is Button -> { + sl = getDisabledColorStateList(color, disabled) + if (view.getBackground() is RippleDrawable) { + val rd = view.getBackground() as RippleDrawable + rd.setColor(ColorStateList.valueOf(rippleColor)) + } + // Disabled text color state for buttons, may get overridden later by ATE tags + view.setTextColor( + getDisabledColorStateList( + textColor, + ContextCompat.getColor( + view.getContext(), + if (useDarkTheme) R.color.ate_button_text_disabled_dark else R.color.ate_button_text_disabled_light + ) + ) + ) + } + is FloatingActionButton -> { + // FloatingActionButton doesn't support disabled state? + sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_pressed) + ), intArrayOf(color, pressed) + ) + + view.rippleColor = rippleColor + view.backgroundTintList = sl + if (view.drawable != null) + view.setImageDrawable(createTintedDrawable(view.drawable, textColor)) + return + } + else -> { + sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf(disabled, color, pressed, activated, activated) + ) + } + } + + var drawable: Drawable? = view.background + if (drawable != null) { + drawable = createTintedDrawable(drawable, sl) + ViewUtils.setBackgroundCompat(view, drawable) + } + + if (view is TextView && view !is Button) { + view.setTextColor( + getDisabledColorStateList( + textColor, + ContextCompat.getColor( + view.getContext(), + if (isColorLight) R.color.ate_text_disabled_light else R.color.ate_text_disabled_dark + ) + ) + ) + } + } + + fun setTintAuto( + view: View, + @ColorInt color: Int, + isBackground: Boolean, + isDark: Boolean + ) { + var isBg = isBackground + if (!isBg) { + when (view) { + is RadioButton -> setTint(view, color, isDark) + is SeekBar -> setTint(view, color, isDark) + is ProgressBar -> setTint(view, color) + is AppCompatEditText -> setTint(view, color, isDark) + is CheckBox -> setTint(view, color, isDark) + is CheckedTextView -> setTint(view, color, isDark) + is ImageView -> setTint(view, color) + is Switch -> setTint(view, color, isDark) + is SwitchCompat -> setTint(view, color, isDark) + is SearchView -> { + val iconIdS = + intArrayOf( + androidx.appcompat.R.id.search_button, + androidx.appcompat.R.id.search_close_btn, + androidx.appcompat.R.id.search_go_btn, + androidx.appcompat.R.id.search_voice_btn, + androidx.appcompat.R.id.search_mag_icon + ) + for (iconId in iconIdS) { + val icon = view.findViewById(iconId) + if (icon != null) { + setTint(icon, color) + } + } + } + else -> isBg = true + } + if (!isBg && view.background is RippleDrawable) { + // Ripples for the above views (e.g. when you tap and hold a switch or checkbox) + val rd = view.background as RippleDrawable + @SuppressLint("PrivateResource") val unchecked = ContextCompat.getColor( + view.context, + if (isDark) androidx.appcompat.R.color.ripple_material_dark else androidx.appcompat.R.color.ripple_material_light + ) + val checked = ColorUtils.adjustAlpha(color, 0.4f) + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_activated, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_activated), + intArrayOf(android.R.attr.state_checked) + ), + intArrayOf(unchecked, checked, checked) + ) + rd.setColor(sl) + } + } + if (isBg) { + // Need to tint the isBackground of a view + if (view is FloatingActionButton || view is Button) { + setTintSelector(view, color, false, isDark) + } else if (view.background != null) { + var drawable: Drawable? = view.background + if (drawable != null) { + drawable = createTintedDrawable(drawable, color) + ViewUtils.setBackgroundCompat(view, drawable) + } + } + } + } + + @SuppressLint("PrivateResource") + fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) { + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), intArrayOf( + // Radio button includes own alpha for disabled state + ColorUtils.stripAlpha( + ContextCompat.getColor( + radioButton.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ) + ), + ContextCompat.getColor( + radioButton.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + radioButton.buttonTintList = sl + } + + fun setTint(seekBar: SeekBar, @ColorInt color: Int, useDarker: Boolean) { + val s1 = getDisabledColorStateList( + color, + ContextCompat.getColor( + seekBar.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ) + ) + seekBar.thumbTintList = s1 + seekBar.progressTintList = s1 + } + + @JvmOverloads + fun setTint( + progressBar: ProgressBar, @ColorInt color: Int, + skipIndeterminate: Boolean = false + ) { + val sl = ColorStateList.valueOf(color) + progressBar.progressTintList = sl + progressBar.secondaryProgressTintList = sl + if (!skipIndeterminate) + progressBar.indeterminateTintList = sl + } + + + @SuppressLint("RestrictedApi") + fun setTint(editText: AppCompatEditText, @ColorInt color: Int, useDarker: Boolean) { + val editTextColorStateList = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf( + android.R.attr.state_enabled, + -android.R.attr.state_pressed, + -android.R.attr.state_focused + ), + intArrayOf() + ), + intArrayOf( + ContextCompat.getColor( + editText.context, + if (useDarker) R.color.ate_text_disabled_dark else R.color.ate_text_disabled_light + ), + ContextCompat.getColor( + editText.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + editText.supportBackgroundTintList = editTextColorStateList + setCursorTint(editText, color) + } + + @SuppressLint("PrivateResource") + fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) { + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf( + ContextCompat.getColor( + box.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ), + ContextCompat.getColor( + box.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + box.buttonTintList = sl + } + + @SuppressLint("PrivateResource") + fun setTint(checkedTextView: CheckedTextView, @ColorInt color: Int, useDarker: Boolean) { + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), intArrayOf( + ContextCompat.getColor( + checkedTextView.context, + if (useDarker) R.color.ate_control_disabled_dark else R.color.ate_control_disabled_light + ), + ContextCompat.getColor( + checkedTextView.context, + if (useDarker) R.color.ate_control_normal_dark else R.color.ate_control_normal_light + ), + color + ) + ) + checkedTextView.checkMarkTintList = sl + TextViewCompat.setCompoundDrawableTintList(checkedTextView, sl) + } + + fun setTint(image: ImageView, @ColorInt color: Int) { + image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + + private fun modifySwitchDrawable( + context: Context, + from: Drawable, + @ColorInt tint: Int, + thumb: Boolean, + compatSwitch: Boolean, + useDarker: Boolean + ): Drawable? { + var tint1 = tint + if (useDarker) { + tint1 = ColorUtils.shiftColor(tint1, 1.1f) + } + tint1 = ColorUtils.adjustAlpha(tint1, if (compatSwitch && !thumb) 0.5f else 1.0f) + val disabled: Int + var normal: Int + if (thumb) { + disabled = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_thumb_disabled_dark else R.color.ate_switch_thumb_disabled_light + ) + normal = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_thumb_normal_dark else R.color.ate_switch_thumb_normal_light + ) + } else { + disabled = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_track_disabled_dark else R.color.ate_switch_track_disabled_light + ) + normal = ContextCompat.getColor( + context, + if (useDarker) R.color.ate_switch_track_normal_dark else R.color.ate_switch_track_normal_light + ) + } + + // Stock switch includes its own alpha + if (!compatSwitch) { + normal = ColorUtils.stripAlpha(normal) + } + + val sl = ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_enabled), + intArrayOf( + android.R.attr.state_enabled, + -android.R.attr.state_activated, + -android.R.attr.state_checked + ), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_activated), + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked) + ), + intArrayOf(disabled, normal, tint1, tint1) + ) + return createTintedDrawable(from, sl) + } + + fun setTint( + @SuppressLint("UseSwitchCompatOrMaterialCode") switchView: Switch, + @ColorInt color: Int, + useDarker: Boolean + ) { + if (switchView.trackDrawable != null) { + switchView.trackDrawable = modifySwitchDrawable( + switchView.context, + switchView.trackDrawable, + color, + thumb = false, + compatSwitch = false, + useDarker = useDarker + ) + } + if (switchView.thumbDrawable != null) { + switchView.thumbDrawable = modifySwitchDrawable( + switchView.context, + switchView.thumbDrawable, + color, + thumb = true, + compatSwitch = false, + useDarker = useDarker + ) + } + } + + fun setTint(switchView: SwitchCompat, @ColorInt color: Int, useDarker: Boolean) { + if (switchView.trackDrawable != null) { + switchView.trackDrawable = modifySwitchDrawable( + switchView.context, + switchView.trackDrawable, + color, + thumb = false, + compatSwitch = true, + useDarker = useDarker + ) + } + if (switchView.thumbDrawable != null) { + switchView.thumbDrawable = modifySwitchDrawable( + switchView.context, + switchView.thumbDrawable, + color, + thumb = true, + compatSwitch = true, + useDarker = useDarker + ) + } + } + + // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise. + @CheckResult + fun createTintedDrawable(drawable: Drawable?, @ColorInt color: Int): Drawable? { + var drawable1: Drawable? = drawable ?: return null + drawable1 = DrawableCompat.wrap(drawable1!!.mutate()) + DrawableCompat.setTintMode(drawable1, PorterDuff.Mode.SRC_IN) + DrawableCompat.setTint(drawable1, color) + return drawable1 + } + + // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise. + @CheckResult + fun createTintedDrawable(drawable: Drawable?, sl: ColorStateList): Drawable? { + var drawable1: Drawable? = drawable ?: return null + drawable1 = DrawableCompat.wrap(drawable1!!.mutate()) + DrawableCompat.setTintList(drawable1, sl) + return drawable1 + } + + @SuppressLint("DiscouragedPrivateApi", "SoonBlockedPrivateApi") + fun setCursorTint(editText: EditText, @ColorInt color: Int) { + try { + val fCursorDrawableRes = TextView::class.java.getDeclaredField("mCursorDrawableRes") + fCursorDrawableRes.isAccessible = true + val mCursorDrawableRes = fCursorDrawableRes.getInt(editText) + val fEditor = TextView::class.java.getDeclaredField("mEditor") + fEditor.isAccessible = true + val editor = fEditor.get(editText) + val clazz = editor.javaClass + val fCursorDrawable = clazz.getDeclaredField("mCursorDrawable") + fCursorDrawable.isAccessible = true + val drawables = arrayOfNulls(2) + drawables[0] = ContextCompat.getDrawable(editText.context, mCursorDrawableRes) + drawables[0] = createTintedDrawable(drawables[0], color) + drawables[1] = ContextCompat.getDrawable(editText.context, mCursorDrawableRes) + drawables[1] = createTintedDrawable(drawables[1], color) + fCursorDrawable.set(editor, drawables) + } catch (ignored: Exception) { + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt b/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt new file mode 100755 index 000000000..931676edd --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/ViewUtils.kt @@ -0,0 +1,43 @@ +package io.legado.app.lib.theme + +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.TransitionDrawable +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.ColorInt +import io.legado.app.utils.DrawableUtils + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +@Suppress("unused") +object ViewUtils { + + fun removeOnGlobalLayoutListener(v: View, listener: ViewTreeObserver.OnGlobalLayoutListener) { + v.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + + fun setBackgroundCompat(view: View, drawable: Drawable?) { + view.background = drawable + } + + fun setBackgroundTransition(view: View, newDrawable: Drawable): TransitionDrawable { + val transition = DrawableUtils.createTransitionDrawable(view.background, newDrawable) + setBackgroundCompat(view, transition) + return transition + } + + fun setBackgroundColorTransition(view: View, @ColorInt newColor: Int): TransitionDrawable { + val oldColor = view.background + + val start = oldColor ?: ColorDrawable(view.solidColor) + val end = ColorDrawable(newColor) + + val transition = DrawableUtils.createTransitionDrawable(start, end) + + setBackgroundCompat(view, transition) + + return transition + } +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeBottomNavigationVIew.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeBottomNavigationVIew.kt new file mode 100755 index 000000000..1d6550182 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeBottomNavigationVIew.kt @@ -0,0 +1,52 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import com.google.android.material.bottomnavigation.BottomNavigationView +import io.legado.app.databinding.ViewNavigationBadgeBinding +import io.legado.app.help.config.AppConfig +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getSecondaryTextColor +import io.legado.app.ui.widget.text.BadgeView +import io.legado.app.utils.ColorUtils + +class ThemeBottomNavigationVIew(context: Context, attrs: AttributeSet) : + BottomNavigationView(context, attrs) { + + init { + val bgColor = context.bottomBackground + setBackgroundColor(bgColor) + val textIsDark = ColorUtils.isColorLight(bgColor) + val textColor = context.getSecondaryTextColor(textIsDark) + val colorStateList = Selector.colorBuild() + .setDefaultColor(textColor) + .setSelectedColor(ThemeStore.accentColor(context)).create() + itemIconTintList = colorStateList + itemTextColor = colorStateList + + if (AppConfig.isEInkMode) { + isItemHorizontalTranslationEnabled = false + itemBackground = ColorDrawable(Color.TRANSPARENT) + } + + ViewCompat.setOnApplyWindowInsetsListener(this, null) + } + + fun addBadgeView(index: Int): BadgeView { + //获取底部菜单view + val menuView = getChildAt(0) as ViewGroup + //获取第index个itemView + val itemView = menuView.getChildAt(index) as ViewGroup + val badgeBinding = ViewNavigationBadgeBinding.inflate(LayoutInflater.from(context)) + itemView.addView(badgeBinding.root) + return badgeBinding.viewBadge + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeCheckBox.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeCheckBox.kt new file mode 100755 index 000000000..2214c6335 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeCheckBox.kt @@ -0,0 +1,37 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatCheckBox +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +class ThemeCheckBox(context: Context, attrs: AttributeSet) : AppCompatCheckBox(context, attrs) { + + private var isUserAction = false + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + } + + override fun performClick(): Boolean { + isUserAction = true + val result = super.performClick() + isUserAction = false + return result + } + + fun setOnUserCheckedChangeListener(listener: ((Boolean) -> Unit)?) { + if (listener == null) { + return super.setOnCheckedChangeListener(null) + } + super.setOnCheckedChangeListener { _, isChecked -> + if (isUserAction) { + listener.invoke(isChecked) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeEditText.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeEditText.kt new file mode 100755 index 000000000..b7607f51a --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeEditText.kt @@ -0,0 +1,23 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatEditText +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +class ThemeEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatEditText(context, attrs) { + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + isLocalePreferredLineHeightForMinimumUsed = false + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeProgressBar.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeProgressBar.kt new file mode 100755 index 000000000..8c0ad0b90 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeProgressBar.kt @@ -0,0 +1,16 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.ProgressBar +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +class ThemeProgressBar(context: Context, attrs: AttributeSet) : ProgressBar(context, attrs) { + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioButton.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioButton.kt new file mode 100755 index 000000000..90f6e958e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioButton.kt @@ -0,0 +1,38 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatRadioButton +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +class ThemeRadioButton(context: Context, attrs: AttributeSet) : + AppCompatRadioButton(context, attrs) { + + private var isUserAction = false + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + } + + override fun performClick(): Boolean { + isUserAction = true + val result = super.performClick() + isUserAction = false + return result + } + + fun setOnUserCheckedChangeListener(listener: ((Boolean) -> Unit)?) { + if (listener == null) { + return super.setOnCheckedChangeListener(null) + } + super.setOnCheckedChangeListener { _, isChecked -> + if (isUserAction) { + listener.invoke(isChecked) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioNoButton.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioNoButton.kt new file mode 100755 index 000000000..3bba9f179 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeRadioNoButton.kt @@ -0,0 +1,83 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatRadioButton +import androidx.appcompat.widget.TooltipCompat +import io.legado.app.R +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.bottomBackground +import io.legado.app.lib.theme.getPrimaryTextColor +import io.legado.app.utils.ColorUtils +import io.legado.app.utils.dpToPx +import io.legado.app.utils.getCompatColor + +class ThemeRadioNoButton(context: Context, attrs: AttributeSet) : + AppCompatRadioButton(context, attrs) { + + private val isBottomBackground: Boolean + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ThemeRadioNoButton) + isBottomBackground = + typedArray.getBoolean(R.styleable.ThemeRadioNoButton_isBottomBackground, false) + typedArray.recycle() + initTheme() + TooltipCompat.setTooltipText(this, text) + } + + private fun initTheme() { + when { + isInEditMode -> Unit + isBottomBackground -> { + val accentColor = context.accentColor + val isLight = ColorUtils.isColorLight(context.bottomBackground) + val textColor = context.getPrimaryTextColor(isLight) + val checkedTextColor = if (ColorUtils.isColorLight(accentColor)) { + Color.BLACK + } else { + Color.WHITE + } + background = Selector.shapeBuild() + .setCornerRadius(2.dpToPx()) + .setStrokeWidth(2.dpToPx()) + .setCheckedBgColor(accentColor) + .setCheckedStrokeColor(accentColor) + .setDefaultStrokeColor(textColor) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(textColor) + .setCheckedColor(checkedTextColor) + .create() + ) + } + else -> { + val accentColor = context.accentColor + val defaultTextColor = context.getCompatColor(R.color.primaryText) + val checkedTextColor = if (ColorUtils.isColorLight(accentColor)) { + Color.BLACK + } else { + Color.WHITE + } + background = Selector.shapeBuild() + .setCornerRadius(2.dpToPx()) + .setStrokeWidth(2.dpToPx()) + .setCheckedBgColor(accentColor) + .setCheckedStrokeColor(accentColor) + .setDefaultStrokeColor(defaultTextColor) + .create() + setTextColor( + Selector.colorBuild() + .setDefaultColor(defaultTextColor) + .setCheckedColor(checkedTextColor) + .create() + ) + } + } + + } + +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeSeekBar.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeSeekBar.kt new file mode 100755 index 000000000..1c842a2e7 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeSeekBar.kt @@ -0,0 +1,19 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatSeekBar +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +/** + * @author Aidan Follestad (afollestad) + */ +class ThemeSeekBar(context: Context, attrs: AttributeSet) : AppCompatSeekBar(context, attrs) { + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ThemeSwitch.kt b/app/src/main/java/io/legado/app/lib/theme/view/ThemeSwitch.kt new file mode 100755 index 000000000..b19bcc51e --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/theme/view/ThemeSwitch.kt @@ -0,0 +1,40 @@ +package io.legado.app.lib.theme.view + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.SwitchCompat +import io.legado.app.lib.theme.accentColor +import io.legado.app.utils.applyTint + +/** + * @author Aidan Follestad (afollestad) + */ +class ThemeSwitch(context: Context, attrs: AttributeSet) : SwitchCompat(context, attrs) { + + private var isUserAction = false + + init { + if (!isInEditMode) { + applyTint(context.accentColor) + } + } + + override fun performClick(): Boolean { + isUserAction = true + val result = super.performClick() + isUserAction = false + return result + } + + fun setOnUserCheckedChangeListener(listener: ((Boolean) -> Unit)?) { + if (listener == null) { + return super.setOnCheckedChangeListener(null) + } + super.setOnCheckedChangeListener { _, isChecked -> + if (isUserAction) { + listener.invoke(isChecked) + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt b/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt new file mode 100755 index 000000000..b3130b7d1 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt @@ -0,0 +1,32 @@ +package io.legado.app.lib.webdav + +import io.legado.app.data.appDb +import io.legado.app.data.entities.Server.WebDavConfig +import okhttp3.Credentials +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +data class Authorization( + val username: String, + val password: String, + val charset: Charset = StandardCharsets.ISO_8859_1 +) { + + var name = "Authorization" + private set + + var data: String = Credentials.basic(username, password, charset) + private set + + override fun toString(): String { + return "$username:$password" + } + + constructor(serverID: Long) : this( + appDb.serverDao.get(serverID)?.getWebDavConfig() + ?: throw WebDavException("Unexpected WebDav Authorization") + ) + + constructor(webDavConfig: WebDavConfig) : this(webDavConfig.username, webDavConfig.password) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt new file mode 100755 index 000000000..352a854d1 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -0,0 +1,457 @@ +package io.legado.app.lib.webdav + +import android.annotation.SuppressLint +import android.net.Uri +import cn.hutool.core.net.URLDecoder +import io.legado.app.constant.AppLog +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.newCallResponse +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.http.text +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.analyzeRule.CustomUrl +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.findNS +import io.legado.app.utils.findNSPrefix +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.toRequestBody +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.intellij.lang.annotations.Language +import org.jsoup.Jsoup +import org.jsoup.parser.Parser +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.net.MalformedURLException +import java.net.URL +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.concurrent.TimeUnit + +@Suppress("unused", "MemberVisibilityCanBePrivate") +open class WebDav( + val path: String, + val authorization: Authorization +) { + companion object { + + fun fromPath(path: String): WebDav { + val id = AnalyzeUrl(path).serverID ?: throw WebDavException("没有serverID") + val authorization = Authorization(id) + return WebDav(path, authorization) + } + + @SuppressLint("DateTimeFormatter") + private val dateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME + + // 指定返回哪些属性 + @Language("xml") + private const val DIR = + """ + + + + + + + + %s + + """ + + @Language("xml") + private const val EXISTS = + """ + + + + + """ + + private const val DEFAULT_CONTENT_TYPE = "application/octet-stream" + } + + + private val url: URL = URL(CustomUrl(path).getUrl()) + private val httpUrl: String? by lazy { + val raw = url.toString() + .replace("davs://", "https://") + .replace("dav://", "http://") + return@lazy kotlin.runCatching { + raw.toHttpUrl().toString() + }.getOrNull() + } + private val webDavClient by lazy { + val authInterceptor = Interceptor { chain -> + var request = chain.request() + if (request.url.host.equals(host, true)) { + request = request + .newBuilder() + .header(authorization.name, authorization.data) + .build() + } + chain.proceed(request) + } + okHttpClient.newBuilder().run { + callTimeout(0, TimeUnit.SECONDS) + interceptors().add(0, authInterceptor) + addNetworkInterceptor(authInterceptor) + build() + } + } + private val host: String? + get() = url.host?.let { + if (it.startsWith("[")) { + it.substring(1, it.lastIndex) + } else { + it + } + } + + /** + * 获取当前url文件信息 + */ + @Throws(WebDavException::class) + suspend fun getWebDavFile(): WebDavFile? { + return propFindResponse(depth = 0)?.let { + parseBody(it).firstOrNull() + } + } + + /** + * 列出当前路径下的文件 + * @return 文件列表 + */ + @Throws(WebDavException::class) + suspend fun listFiles(): List { + propFindResponse()?.let { body -> + return parseBody(body).filter { + it.path != path + } + } + return emptyList() + } + + /** + * @param propsList 指定列出文件的哪些属性 + */ + @Throws(WebDavException::class) + private suspend fun propFindResponse( + propsList: List = emptyList(), + depth: Int = 1 + ): String? { + val requestProps = StringBuilder() + for (p in propsList) { + requestProps.append("\n") + } + val requestPropsStr: String = if (requestProps.toString().isEmpty()) { + DIR.replace("%s", "") + } else { + String.format(DIR, requestProps.toString() + "\n") + } + val url = httpUrl ?: return null + return webDavClient.newCallResponse { + url(url) + addHeader("Depth", depth.toString()) + // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 + // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 + val requestBody = requestPropsStr.toRequestBody("text/plain".toMediaType()) + method("PROPFIND", requestBody) + }.apply { + checkResult(this) + }.body.text() + } + + /** + * 解析webDav返回的xml + */ + private fun parseBody(s: String): List { + val list = ArrayList() + val document = kotlin.runCatching { + Jsoup.parse(s, Parser.xmlParser()) + }.getOrElse { + Jsoup.parse(s) + } + val ns = document.findNSPrefix("DAV:") + val elements = document.findNS("response", ns) + val urlStr = httpUrl ?: return list + val baseUrl = NetworkUtils.getBaseUrl(urlStr) + for (element in elements) { + //依然是优化支持 caddy 自建的 WebDav ,其目录后缀都为“/”, 所以删除“/”的判定,不然无法获取该目录项 + val href = element.findNS("href", ns)[0].text() + val hrefDecode = URLDecoder.decodeForPath(href, Charsets.UTF_8) + val fileName = hrefDecode.removeSuffix("/").substringAfterLast("/") + val webDavFile: WebDav + try { + val urlName = hrefDecode.ifEmpty { + url.file.replace("/", "") + } + val displayName = element + .findNS("displayname", ns) + .firstOrNull()?.text()?.takeIf { it.isNotEmpty() } + ?.let { URLDecoder.decodeForPath(it, Charsets.UTF_8) } ?: fileName + val contentType = element + .findNS("getcontenttype", ns) + .firstOrNull()?.text().orEmpty() + val resourceType = element + .findNS("resourcetype", ns) + .firstOrNull()?.html()?.trim().orEmpty() + val size = kotlin.runCatching { + element.findNS("getcontentlength", ns) + .firstOrNull()?.text()?.toLong() ?: 0 + }.getOrDefault(0) + val lastModify: Long = kotlin.runCatching { + element.findNS("getlastmodified", ns) + .firstOrNull()?.text()?.let { + ZonedDateTime.parse(it, dateTimeFormatter) + .toInstant().toEpochMilli() + } + }.getOrNull() ?: 0 + var fullURL = NetworkUtils.getAbsoluteURL(baseUrl, hrefDecode) + if (WebDavFile.isDir(contentType, resourceType) && !fullURL.endsWith("/")) { + fullURL += "/" + } + webDavFile = WebDavFile( + fullURL, + authorization, + displayName = displayName, + urlName = urlName, + size = size, + contentType = contentType, + resourceType = resourceType, + lastModify = lastModify + ) + list.add(webDavFile) + } catch (e: MalformedURLException) { + e.printOnDebug() + } + } + return list + } + + /** + * 文件是否存在 + */ + suspend fun exists(): Boolean { + val url = httpUrl ?: return false + return kotlin.runCatching { + return webDavClient.newCallResponse { + url(url) + addHeader("Depth", "0") + val requestBody = EXISTS.toRequestBody("application/xml".toMediaType()) + method("PROPFIND", requestBody) + }.use { it.isSuccessful } + }.onFailure { + currentCoroutineContext().ensureActive() + }.getOrDefault(false) + } + + /** + * 检查用户名密码是否有效 + */ + suspend fun check(): Boolean { + return kotlin.runCatching { + webDavClient.newCallResponse { + url(url) + addHeader("Depth", "0") + val requestBody = EXISTS.toRequestBody("application/xml".toMediaType()) + method("PROPFIND", requestBody) + }.use { it.code != 401 } + }.onFailure { + currentCoroutineContext().ensureActive() + }.getOrDefault(true) + } + + /** + * 根据自己的URL,在远程处创建对应的文件夹 + * @return 是否创建成功 + */ + suspend fun makeAsDir(): Boolean { + val url = httpUrl ?: return false + //防止报错 + return kotlin.runCatching { + if (!exists()) { + webDavClient.newCallResponse { + url(url) + method("MKCOL", null) + }.use { + checkResult(it) + } + } + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav创建目录失败\n${it.localizedMessage}", it) + }.isSuccess + } + + /** + * 下载到本地 + * @param savedPath 本地的完整路径,包括最后的文件名 + * @param replaceExisting 是否替换本地的同名文件 + */ + @Throws(WebDavException::class) + suspend fun downloadTo(savedPath: String, replaceExisting: Boolean) { + val file = File(savedPath) + if (file.exists() && !replaceExisting) { + return + } + downloadInputStream().use { byteStream -> + FileOutputStream(file).use { + byteStream.copyTo(it) + } + } + } + + /** + * 下载文件,返回ByteArray + */ + @Throws(WebDavException::class) + suspend fun download(): ByteArray { + return downloadInputStream().use { + it.readBytes() + } + } + + /** + * 上传文件 + */ + @Throws(WebDavException::class) + suspend fun upload(localPath: String, contentType: String = DEFAULT_CONTENT_TYPE) { + upload(File(localPath), contentType) + } + + @Throws(WebDavException::class) + suspend fun upload(file: File, contentType: String = DEFAULT_CONTENT_TYPE) { + kotlin.runCatching { + withContext(IO) { + if (!file.exists()) throw WebDavException("文件不存在") + // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 + val fileBody = file.asRequestBody(contentType.toMediaType()) + val url = httpUrl ?: throw WebDavException("url不能为空") + webDavClient.newCallResponse { + url(url) + put(fileBody) + }.use { + checkResult(it) + } + } + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav上传失败\n${it.localizedMessage}", it) + throw WebDavException("WebDav上传失败\n${it.localizedMessage}") + } + } + + @Throws(WebDavException::class) + suspend fun upload(byteArray: ByteArray, contentType: String = DEFAULT_CONTENT_TYPE) { + // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 + kotlin.runCatching { + withContext(IO) { + val fileBody = byteArray.toRequestBody(contentType.toMediaType()) + val url = httpUrl ?: throw NoStackTraceException("url不能为空") + webDavClient.newCallResponse { + url(url) + put(fileBody) + }.use { + checkResult(it) + } + } + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav上传失败\n${it.localizedMessage}", it) + throw WebDavException("WebDav上传失败\n${it.localizedMessage}") + } + } + + @Throws(WebDavException::class) + suspend fun upload(uri: Uri, contentType: String = DEFAULT_CONTENT_TYPE) { + // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 + kotlin.runCatching { + withContext(IO) { + val fileBody = uri.toRequestBody(contentType.toMediaType()) + val url = httpUrl ?: throw NoStackTraceException("url不能为空") + webDavClient.newCallResponse { + url(url) + put(fileBody) + }.use { + checkResult(it) + } + } + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav上传失败\n${it.localizedMessage}", it) + throw WebDavException("WebDav上传失败\n${it.localizedMessage}") + } + } + + @Throws(WebDavException::class) + suspend fun downloadInputStream(): InputStream { + val url = httpUrl ?: throw WebDavException("WebDav下载出错\nurl为空") + val byteStream = webDavClient.newCallResponse { + url(url) + }.apply { + checkResult(this) + }.body.byteStream() + return byteStream + } + + /** + * 移除文件/文件夹 + */ + suspend fun delete(): Boolean { + val url = httpUrl ?: return false + //防止报错 + return kotlin.runCatching { + webDavClient.newCallResponse { + url(url) + method("DELETE", null) + }.use { + checkResult(it) + } + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("WebDav删除失败\n${it.localizedMessage}", it) + }.isSuccess + } + + /** + * 检测返回结果是否正确 + */ + private fun checkResult(response: Response) { + if (!response.isSuccessful) { + val body = response.body.string() + if (response.code == 401) { + val headers = response.headers("WWW-Authenticate") + val supportBasicAuth = headers.any { + it.startsWith("Basic", ignoreCase = true) + } + if (headers.isNotEmpty() && !supportBasicAuth) { + AppLog.put("服务器不支持BasicAuth认证") + } + } + + if (response.message.isNotBlank() || body.isBlank()) { + throw WebDavException("${url}\n${response.code}:${response.message}") + } + val document = Jsoup.parse(body) + val exception = document.getElementsByTag("s:exception").firstOrNull()?.text() + val message = document.getElementsByTag("s:message").firstOrNull()?.text() + if (exception == "ObjectNotFound") { + throw ObjectNotFoundException( + message ?: "$path doesn't exist. code:${response.code}" + ) + } + throw WebDavException(message ?: "未知错误 code:${response.code}") + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDavException.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDavException.kt new file mode 100755 index 000000000..210fff73d --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDavException.kt @@ -0,0 +1,11 @@ +package io.legado.app.lib.webdav + +open class WebDavException(msg: String) : Exception(msg) { + + override fun fillInStackTrace(): Throwable { + return this + } + +} + +class ObjectNotFoundException(msg: String) : WebDavException(msg) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt new file mode 100755 index 000000000..70b9becd6 --- /dev/null +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt @@ -0,0 +1,29 @@ +package io.legado.app.lib.webdav + +/** + * webDavFile + */ +@Suppress("unused") +class WebDavFile( + urlStr: String, + authorization: Authorization, + val displayName: String, + val urlName: String, + val size: Long, + val contentType: String, + val resourceType: String, + val lastModify: Long +) : WebDav(urlStr, authorization) { + + val isDir by lazy { + isDir(contentType, resourceType) + } + + companion object { + fun isDir(contentType: String, resourceType: String): Boolean { + return contentType == "httpd/unix-directory" + || resourceType.lowercase().contains("collection") + } + } + +} diff --git a/app/src/main/java/io/legado/app/model/AudioPlay.kt b/app/src/main/java/io/legado/app/model/AudioPlay.kt new file mode 100755 index 000000000..eae7b9d26 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/AudioPlay.kt @@ -0,0 +1,433 @@ +package io.legado.app.model + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import io.legado.app.R +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.constant.Status +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.getBookSource +import io.legado.app.help.book.readSimulating +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.book.update +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.AudioPlayService +import io.legado.app.utils.postEvent +import io.legado.app.utils.startService +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancelChildren +import splitties.init.appCtx + +@SuppressLint("StaticFieldLeak") +@Suppress("unused") +object AudioPlay : CoroutineScope by MainScope() { + /** + * 播放模式枚举 + */ + enum class PlayMode(val iconRes: Int) { + LIST_END_STOP(R.drawable.ic_play_mode_list_end_stop), + SINGLE_LOOP(R.drawable.ic_play_mode_single_loop), + RANDOM(R.drawable.ic_play_mode_random), + LIST_LOOP(R.drawable.ic_play_mode_list_loop); + + fun next(): PlayMode { + return when (this) { + LIST_END_STOP -> SINGLE_LOOP + SINGLE_LOOP -> RANDOM + RANDOM -> LIST_LOOP + LIST_LOOP -> LIST_END_STOP + } + } + } + + var playMode = PlayMode.LIST_END_STOP + var status = Status.STOP + private var activityContext: Context? = null + private var serviceContext: Context? = null + private val context: Context get() = activityContext ?: serviceContext ?: appCtx + var callback: CallBack? = null + var book: Book? = null + var chapterSize = 0 + var simulatedChapterSize = 0 + var durChapterIndex = 0 + var durChapterPos = 0 + var durChapter: BookChapter? = null + var durPlayUrl = "" + var durAudioSize = 0 + var inBookshelf = false + var bookSource: BookSource? = null + val loadingChapters = arrayListOf() + + fun changePlayMode() { + playMode = playMode.next() + postEvent(EventBus.PLAY_MODE_CHANGED, playMode) + } + + fun upData(book: Book) { + AudioPlay.book = book + chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) + simulatedChapterSize = if (book.readSimulating()) { + book.simulatedTotalChapterNum() + } else { + chapterSize + } + if (durChapterIndex != book.durChapterIndex) { + stopPlay() + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + durPlayUrl = "" + durAudioSize = 0 + } + upDurChapter() + } + + fun resetData(book: Book) { + stop() + AudioPlay.book = book + chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) + simulatedChapterSize = if (book.readSimulating()) { + book.simulatedTotalChapterNum() + } else { + chapterSize + } + bookSource = book.getBookSource() + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + durPlayUrl = "" + durAudioSize = 0 + upDurChapter() + postEvent(EventBus.AUDIO_BUFFER_PROGRESS, 0) + } + + private fun addLoading(index: Int): Boolean { + synchronized(this) { + if (loadingChapters.contains(index)) return false + loadingChapters.add(index) + return true + } + } + + private fun removeLoading(index: Int) { + synchronized(this) { + loadingChapters.remove(index) + } + } + + fun loadOrUpPlayUrl() { + if (durPlayUrl.isEmpty()) { + loadPlayUrl() + } else { + upPlayUrl() + } + } + + /** + * 加载播放URL + */ + private fun loadPlayUrl() { + val index = durChapterIndex + if (addLoading(index)) { + val book = book + val bookSource = bookSource + if (book != null && bookSource != null) { + upDurChapter() + val chapter = durChapter + if (chapter == null) { + removeLoading(index) + return + } + if (chapter.isVolume) { + skipTo(index + 1) + removeLoading(index) + return + } + upLoading(true) + WebBook.getContent(this, bookSource, book, chapter) + .onSuccess { content -> + if (content.isEmpty()) { + appCtx.toastOnUi("未获取到资源链接") + } else { + contentLoadFinish(chapter, content) + } + }.onError { + AppLog.put("获取资源链接出错\n$it", it, true) + upLoading(false) + }.onCancel { + removeLoading(index) + }.onFinally { + removeLoading(index) + } + } else { + removeLoading(index) + appCtx.toastOnUi("book or source is null") + } + } + } + + /** + * 加载完成 + */ + private fun contentLoadFinish(chapter: BookChapter, content: String) { + if (chapter.index == book?.durChapterIndex) { + durPlayUrl = content + upPlayUrl() + } + } + + private fun upPlayUrl() { + if (isPlayToEnd()) { + playNew() + } else { + play() + } + } + + /** + * 播放当前章节 + */ + fun play() { + context.startService { + action = IntentAction.play + } + } + + /** + * 从头播放新章节 + */ + private fun playNew() { + context.startService { + action = IntentAction.playNew + } + } + + /** + * 更新当前章节 + */ + fun upDurChapter() { + val book = book ?: return + durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex) + durAudioSize = durChapter?.end?.toInt() ?: 0 + val title = durChapter?.title ?: appCtx.getString(R.string.data_loading) + postEvent(EventBus.AUDIO_SUB_TITLE, title) + postEvent(EventBus.AUDIO_SIZE, durAudioSize) + postEvent(EventBus.AUDIO_PROGRESS, durChapterPos) + } + + fun pause(context: Context) { + if (AudioPlayService.isRun) { + context.startService { + action = IntentAction.pause + } + } + } + + fun resume(context: Context) { + if (AudioPlayService.isRun) { + context.startService { + action = IntentAction.resume + } + } + } + + fun stop() { + if (AudioPlayService.isRun) { + context.startService { + action = IntentAction.stop + } + } + } + + fun adjustSpeed(adjust: Float) { + if (AudioPlayService.isRun) { + context.startService { + action = IntentAction.adjustSpeed + putExtra("adjust", adjust) + } + } + } + + fun adjustProgress(position: Int) { + durChapterPos = position + saveRead() + if (AudioPlayService.isRun) { + context.startService { + action = IntentAction.adjustProgress + putExtra("position", position) + } + } + } + + fun skipTo(index: Int) { + Coroutine.async { + stopPlay() + if (index in 0.. 0) { + durChapterIndex -= 1 + durChapterPos = 0 + durPlayUrl = "" + saveRead() + loadPlayUrl() + } + } + } + + fun next() { + stopPlay() + when (playMode) { + PlayMode.LIST_END_STOP -> { + if (durChapterIndex + 1 < simulatedChapterSize) { + durChapterIndex += 1 + durChapterPos = 0 + durPlayUrl = "" + saveRead() + loadPlayUrl() + } + } + + PlayMode.SINGLE_LOOP -> { + durChapterPos = 0 + durPlayUrl = "" + saveRead() + loadPlayUrl() + } + + PlayMode.RANDOM -> { + durChapterIndex = (0 until simulatedChapterSize).random() + durChapterPos = 0 + durPlayUrl = "" + saveRead() + loadPlayUrl() + } + + PlayMode.LIST_LOOP -> { + durChapterIndex = (durChapterIndex + 1) % simulatedChapterSize + durChapterPos = 0 + durPlayUrl = "" + saveRead() + loadPlayUrl() + } + } + } + + fun setTimer(minute: Int) { + if (AudioPlayService.isRun) { + val intent = Intent(context, AudioPlayService::class.java) + intent.action = IntentAction.setTimer + intent.putExtra("minute", minute) + context.startService(intent) + } else { + AudioPlayService.timeMinute = minute + postEvent(EventBus.AUDIO_DS, minute) + } + } + + fun addTimer() { + val intent = Intent(context, AudioPlayService::class.java) + intent.action = IntentAction.addTimer + context.startService(intent) + } + + fun stopPlay() { + if (AudioPlayService.isRun) { + context.startService { + action = IntentAction.stopPlay + } + } + } + + fun saveRead() { + val book = book ?: return + Coroutine.async { + book.lastCheckCount = 0 + book.durChapterTime = System.currentTimeMillis() + val chapterChanged = book.durChapterIndex != durChapterIndex + book.durChapterIndex = durChapterIndex + book.durChapterPos = durChapterPos + if (chapterChanged) { + appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let { + book.durChapterTitle = it.getDisplayTitle( + ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(), + book.getUseReplaceRule() + ) + } + } + book.update() + } + } + + /** + * 保存章节长度 + */ + fun saveDurChapter(audioSize: Long) { + val chapter = durChapter ?: return + Coroutine.async { + durAudioSize = audioSize.toInt() + chapter.end = audioSize + appDb.bookChapterDao.update(chapter) + } + } + + fun playPositionChanged(position: Int) { + durChapterPos = position + saveRead() + } + + fun upLoading(loading: Boolean) { + callback?.upLoading(loading) + } + + private fun isPlayToEnd(): Boolean { + return durChapterIndex + 1 == simulatedChapterSize + && durChapterPos == durAudioSize + } + + fun register(context: Context) { + activityContext = context + callback = context as CallBack + } + + fun unregister(context: Context) { + if (activityContext === context) { + activityContext = null + callback = null + } + coroutineContext.cancelChildren() + } + + fun registerService(context: Context) { + serviceContext = context + } + + fun unregisterService() { + serviceContext = null + } + + interface CallBack { + + fun upLoading(loading: Boolean) + + } + +} diff --git a/app/src/main/java/io/legado/app/model/BookCover.kt b/app/src/main/java/io/legado/app/model/BookCover.kt new file mode 100755 index 000000000..ee922a41a --- /dev/null +++ b/app/src/main/java/io/legado/app/model/BookCover.kt @@ -0,0 +1,269 @@ +package io.legado.app.model + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.annotation.Keep +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.Book +import io.legado.app.help.CacheManager +import io.legado.app.help.DefaultData +import io.legado.app.help.config.AppConfig +import io.legado.app.help.glide.BlurTransformation +import io.legado.app.help.glide.ImageLoader +import io.legado.app.help.glide.OkHttpModelLoader +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.utils.BitmapUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefString +import kotlinx.coroutines.currentCoroutineContext +import splitties.init.appCtx +import java.io.File + +@Keep +@Suppress("ConstPropertyName") +object BookCover { + + private const val coverRuleConfigKey = "legadoCoverRuleConfig" + const val configFileName = "coverRule.json" + + var drawBookName = true + private set + var drawBookAuthor = true + private set + lateinit var defaultDrawable: Drawable + private set + + + init { + upDefaultCover() + } + + @SuppressLint("UseCompatLoadingForDrawables") + fun upDefaultCover() { + val isNightTheme = AppConfig.isNightTheme + drawBookName = if (isNightTheme) { + appCtx.getPrefBoolean(PreferKey.coverShowNameN, true) + } else { + appCtx.getPrefBoolean(PreferKey.coverShowName, true) + } + drawBookAuthor = if (isNightTheme) { + appCtx.getPrefBoolean(PreferKey.coverShowAuthorN, true) + } else { + appCtx.getPrefBoolean(PreferKey.coverShowAuthor, true) + } + val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover + val path = appCtx.getPrefString(key) + if (path.isNullOrBlank()) { + defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null) + return + } + defaultDrawable = kotlin.runCatching { + BitmapDrawable(appCtx.resources, BitmapUtils.decodeBitmap(path, 600, 900)) + }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null)) + } + + /** + * 加载封面 + */ + fun load( + context: Context, + path: String?, + loadOnlyWifi: Boolean = false, + sourceOrigin: String? = null, + onLoadFinish: (() -> Unit)? = null, + ): RequestBuilder { + if (AppConfig.useDefaultCover) { + return ImageLoader.load(context, defaultDrawable) + .centerCrop() + } + var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi) + if (sourceOrigin != null) { + options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin) + } + var builder = ImageLoader.load(context, path) + .apply(options) + if (onLoadFinish != null) { + builder = builder.addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean, + ): Boolean { + onLoadFinish.invoke() + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean, + ): Boolean { + onLoadFinish.invoke() + return false + } + }) + } + return builder.placeholder(defaultDrawable) + .error(defaultDrawable) + .centerCrop() + } + + /** + * 加载漫画图片 + */ + fun loadManga( + context: Context, + path: String?, + loadOnlyWifi: Boolean = false, + sourceOrigin: String? = null, + transformation: Transformation? = null, + ): RequestBuilder { + var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi) + .set(OkHttpModelLoader.mangaOption, true) + if (sourceOrigin != null) { + options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin) + } + return ImageLoader.load(context, path) + .apply(options) + .override(context.resources.displayMetrics.widthPixels, SIZE_ORIGINAL) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .skipMemoryCache(true).let { + if (transformation != null) { + it.transform(transformation) + } else { + it + } + } + } + + fun preloadManga( + context: Context, + path: String?, + loadOnlyWifi: Boolean = false, + sourceOrigin: String? = null, + ): RequestBuilder { + var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi) + .set(OkHttpModelLoader.mangaOption, true) + if (sourceOrigin != null) { + options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin) + } + return Glide.with(context) + .downloadOnly() + .apply(options) + .load(path) + } + + /** + * 加载模糊封面 + */ + fun loadBlur( + context: Context, + path: String?, + loadOnlyWifi: Boolean = false, + sourceOrigin: String? = null, + ): RequestBuilder { + val loadBlur = ImageLoader.load(context, defaultDrawable) + .transform(BlurTransformation(25), CenterCrop()) + if (AppConfig.useDefaultCover) { + return loadBlur + } + var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi) + if (sourceOrigin != null) { + options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin) + } + return ImageLoader.load(context, path) + .apply(options) + .transform(BlurTransformation(25), CenterCrop()) + .transition(DrawableTransitionOptions.withCrossFade(1500)) + .thumbnail(loadBlur) + } + + fun getCoverRule(): CoverRule { + return getConfig() ?: DefaultData.coverRule + } + + fun getConfig(): CoverRule? { + return GSON.fromJsonObject(CacheManager.get(coverRuleConfigKey)) + .getOrNull() + } + + suspend fun searchCover(book: Book): String? { + val config = getCoverRule() + if (!config.enable || config.searchUrl.isBlank() || config.coverRule.isBlank()) { + return null + } + val analyzeUrl = AnalyzeUrl( + config.searchUrl, + book.name, + source = config, + coroutineContext = currentCoroutineContext(), + hasLoginHeader = false + ) + val res = analyzeUrl.getStrResponseAwait() + val analyzeRule = AnalyzeRule(book) + analyzeRule.setCoroutineContext(currentCoroutineContext()) + analyzeRule.setContent(res.body) + analyzeRule.setRedirectUrl(res.url) + return analyzeRule.getString(config.coverRule, isUrl = true) + } + + fun saveCoverRule(config: CoverRule) { + val json = GSON.toJson(config) + saveCoverRule(json) + } + + fun saveCoverRule(json: String) { + CacheManager.put(coverRuleConfigKey, json) + } + + fun delCoverRule() { + CacheManager.delete(coverRuleConfigKey) + } + + @Keep + data class CoverRule( + var enable: Boolean = true, + var searchUrl: String, + var coverRule: String, + override var concurrentRate: String? = null, + override var loginUrl: String? = null, + override var loginUi: String? = null, + override var header: String? = null, + override var jsLib: String? = null, + override var enabledCookieJar: Boolean? = false, + ) : BaseSource { + + override fun getTag(): String { + return searchUrl + } + + override fun getKey(): String { + return searchUrl + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/CacheBook.kt b/app/src/main/java/io/legado/app/model/CacheBook.kt new file mode 100755 index 000000000..aa12fdf54 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/CacheBook.kt @@ -0,0 +1,460 @@ +package io.legado.app.model + +import android.content.Context +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.ConcurrentException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isLocal +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.CacheBookService +import io.legado.app.utils.onEachParallel +import io.legado.app.utils.postEvent +import io.legado.app.utils.startService +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.isActive +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.CoroutineContext + +object CacheBook { + + val cacheBookMap = ConcurrentHashMap() + + private val workingState = MutableStateFlow(true) + private val mutex = Mutex() + + @Synchronized + fun getOrCreate(bookUrl: String): CacheBookModel? { + val book = appDb.bookDao.getBook(bookUrl) ?: return null + val bookSource = appDb.bookSourceDao.getBookSource(book.origin) ?: return null + updateBookSource(bookSource) + var cacheBook = cacheBookMap[bookUrl] + if (cacheBook != null) { + //存在时更新,书源可能会变化,必须更新 + cacheBook.bookSource = bookSource + cacheBook.book = book + return cacheBook + } + cacheBook = CacheBookModel(bookSource, book) + cacheBookMap[bookUrl] = cacheBook + return cacheBook + } + + @Synchronized + fun getOrCreate(bookSource: BookSource, book: Book): CacheBookModel { + updateBookSource(bookSource) + var cacheBook = cacheBookMap[book.bookUrl] + if (cacheBook != null) { + //存在时更新,书源可能会变化,必须更新 + cacheBook.bookSource = bookSource + cacheBook.book = book + return cacheBook + } + cacheBook = CacheBookModel(bookSource, book) + cacheBookMap[book.bookUrl] = cacheBook + return cacheBook + } + + private fun updateBookSource(newBookSource: BookSource) { + cacheBookMap.forEach { + val model = it.value + if (model.bookSource.bookSourceUrl == newBookSource.bookSourceUrl) { + model.bookSource = newBookSource + } + } + } + + fun start(context: Context, book: Book, start: Int, end: Int) { + if (!book.isLocal) { + context.startService { + action = IntentAction.start + putExtra("bookUrl", book.bookUrl) + putExtra("start", start) + putExtra("end", end) + } + } + } + + fun remove(context: Context, bookUrl: String) { + context.startService { + action = IntentAction.remove + putExtra("bookUrl", bookUrl) + } + } + + fun stop(context: Context) { + if (CacheBookService.isRun) { + context.startService { + action = IntentAction.stop + } + } + } + + fun close() { + cacheBookMap.forEach { it.value.stop() } + cacheBookMap.clear() + successDownloadSet.clear() + errorDownloadMap.clear() + } + + fun setWorkingState(value: Boolean) { + workingState.value = value + } + + suspend fun startProcessJob(context: CoroutineContext) = mutex.withLock { + setWorkingState(true) + flow { + while (currentCoroutineContext().isActive && cacheBookMap.isNotEmpty()) { + var emitted = false + + cacheBookMap.forEach { (_, model) -> + if (!model.isLoading()) { + emit(model) + emitted = true + } + workingState.first { it } + } + + if (!emitted) { + delay(1000) + } + } + }.onStart { + postEvent(EventBus.UP_DOWNLOAD_STATE, "") + }.onEachParallel(AppConfig.threadCount) { + coroutineScope { + it.download(this, context) + } + }.onCompletion { + postEvent(EventBus.UP_DOWNLOAD_STATE, "") + }.collect() + } + + + val downloadSummary: String + get() { + return "正在下载:${onDownloadCount}|等待中:${waitCount}|失败:${errorDownloadMap.count()}|成功:${successDownloadSet.size}" + } + + val isRun: Boolean + get() { + cacheBookMap.forEach { + if (it.value.isRun()) { + return true + } + } + return false + } + + private val waitCount: Int + get() { + var count = 0 + cacheBookMap.forEach { + count += it.value.waitCount + } + return count + } + + val onDownloadCount: Int + get() { + var count = 0 + cacheBookMap.forEach { + count += it.value.onDownloadCount + } + return count + } + + val successDownloadSet = linkedSetOf() + val errorDownloadMap = hashMapOf() + + class CacheBookModel(var bookSource: BookSource, var book: Book) { + + private val waitDownloadSet = linkedSetOf() + private val onDownloadSet = linkedSetOf() + private val tasks = CompositeCoroutine() + private var isStopped = false + private var waitingRetry = false + private var isLoading = false + + val waitCount get() = waitDownloadSet.size + val onDownloadCount get() = onDownloadSet.size + + init { + postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) + } + + @Synchronized + fun isRun(): Boolean { + return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty() || isLoading + } + + @Synchronized + fun isStop(): Boolean { + return isStopped || (!isRun() && !waitingRetry) + } + + @Synchronized + fun isLoading(): Boolean { + return isLoading + } + + @Synchronized + fun setLoading() { + isLoading = true + } + + @Synchronized + fun stop() { + waitDownloadSet.clear() + tasks.clear() + isStopped = true + isLoading = false + postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) + } + + @Synchronized + fun addDownload(start: Int, end: Int) { + isStopped = false + for (i in start..end) { + if (!onDownloadSet.contains(i)) { + waitDownloadSet.add(i) + } + } + cacheBookMap[book.bookUrl] = this + isLoading = false + postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) + } + + @Synchronized + private fun onSuccess(chapter: BookChapter) { + onDownloadSet.remove(chapter.index) + successDownloadSet.add(chapter.primaryStr()) + errorDownloadMap.remove(chapter.primaryStr()) + } + + @Synchronized + private fun onPreError(chapter: BookChapter, error: Throwable) { + waitingRetry = true + if (error !is ConcurrentException) { + errorDownloadMap[chapter.primaryStr()] = + (errorDownloadMap[chapter.primaryStr()] ?: 0) + 1 + } + onDownloadSet.remove(chapter.index) + } + + @Synchronized + private fun onPostError(chapter: BookChapter, error: Throwable) { + //重试3次 + if ((errorDownloadMap[chapter.primaryStr()] ?: 0) < 3 && !isStopped) { + waitDownloadSet.add(chapter.index) + } else { + AppLog.put( + "下载${book.name}-${chapter.title}失败\n${error.localizedMessage}", + error + ) + } + waitingRetry = false + } + + @Synchronized + private fun onError(chapter: BookChapter, error: Throwable) { + onPreError(chapter, error) + onPostError(chapter, error) + } + + @Synchronized + private fun onCancel(index: Int) { + onDownloadSet.remove(index) + if (!isStopped) waitDownloadSet.add(index) + } + + @Synchronized + private fun onFinally() { + if (waitDownloadSet.isEmpty() && onDownloadSet.isEmpty()) { + cacheBookMap.remove(book.bookUrl) + } + postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) + } + + /** + * 从待下载列表内取第一条下载 + */ + @Synchronized + fun download(scope: CoroutineScope, context: CoroutineContext) { + val chapterIndex = waitDownloadSet.firstOrNull() + if (chapterIndex == null) { + if (!isLoading && onDownloadSet.isEmpty()) { + cacheBookMap.remove(book.bookUrl) + } + return + } + if (onDownloadSet.contains(chapterIndex)) { + waitDownloadSet.remove(chapterIndex) + return + } + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, chapterIndex) ?: let { + waitDownloadSet.remove(chapterIndex) + return + } + if (chapter.isVolume) { + /** 修正下载计数 */ + postEvent(EventBus.SAVE_CONTENT, Pair(book, chapter)) + waitDownloadSet.remove(chapterIndex) + return + } + if (BookHelp.hasImageContent(book, chapter)) { + waitDownloadSet.remove(chapterIndex) + return + } + waitDownloadSet.remove(chapterIndex) + onDownloadSet.add(chapterIndex) + if (BookHelp.hasContent(book, chapter)) { + Coroutine.async(scope, context, executeContext = context) { + BookHelp.getContent(book, chapter)?.let { + BookHelp.saveImages(bookSource, book, chapter, it, 1) + } + }.onSuccess { + onSuccess(chapter) + }.onError { + onPreError(chapter, it) + //出现错误等待一秒后重新加入待下载列表 + delay(1000) + onPostError(chapter, it) + }.onCancel { + onCancel(chapterIndex) + }.onFinally { + onFinally() + }.let { + tasks.add(it) + } + return + } + WebBook.getContent( + scope, + bookSource, + book, + chapter, + context = context, + start = CoroutineStart.LAZY, + executeContext = context + ).onSuccess { content -> + onSuccess(chapter) + downloadFinish(chapter, content) + }.onError { + onPreError(chapter, it) + //出现错误等待一秒后重新加入待下载列表 + delay(1000) + onPostError(chapter, it) + downloadFinish(chapter, "获取正文失败\n${it.localizedMessage}") + }.onCancel { + onCancel(chapterIndex) + }.onFinally { + onFinally() + }.apply { + tasks.add(this) + }.start() + } + + suspend fun downloadAwait(chapter: BookChapter): String { + synchronized(this) { + onDownloadSet.add(chapter.index) + waitDownloadSet.remove(chapter.index) + } + try { + val content = WebBook.getContentAwait(bookSource, book, chapter) + onSuccess(chapter) + ReadBook.downloadedChapters.add(chapter.index) + ReadBook.downloadFailChapters.remove(chapter.index) + return content + } catch (e: Exception) { + if (e is CancellationException) { + onCancel(chapter.index) + } + onError(chapter, e) + ReadBook.downloadFailChapters[chapter.index] = + (ReadBook.downloadFailChapters[chapter.index] ?: 0) + 1 + return "获取正文失败\n${e.localizedMessage}" + } finally { + postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) + } + } + + @Synchronized + fun download( + scope: CoroutineScope, + chapter: BookChapter, + semaphore: Semaphore?, + resetPageOffset: Boolean = false + ) { + if (onDownloadSet.contains(chapter.index)) { + return + } + onDownloadSet.add(chapter.index) + waitDownloadSet.remove(chapter.index) + WebBook.getContent( + scope, + bookSource, + book, + chapter, + start = CoroutineStart.LAZY, + executeContext = IO, + semaphore = semaphore + ).onSuccess { content -> + onSuccess(chapter) + ReadBook.downloadedChapters.add(chapter.index) + ReadBook.downloadFailChapters.remove(chapter.index) + downloadFinish(chapter, content, resetPageOffset) + }.onError { + onError(chapter, it) + ReadBook.downloadFailChapters[chapter.index] = + (ReadBook.downloadFailChapters[chapter.index] ?: 0) + 1 + downloadFinish(chapter, "获取正文失败\n${it.localizedMessage}", resetPageOffset) + }.onCancel { + onCancel(chapter.index) + downloadFinish(chapter, "download canceled", resetPageOffset, true) + }.onFinally { + postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) + }.start() + } + + private fun downloadFinish( + chapter: BookChapter, + content: String, + resetPageOffset: Boolean = false, + canceled: Boolean = false + ) { + if (ReadBook.book?.bookUrl == book.bookUrl) { + ReadBook.contentLoadFinish( + book, chapter, content, + resetPageOffset = resetPageOffset, + canceled = canceled + ) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/CheckSource.kt b/app/src/main/java/io/legado/app/model/CheckSource.kt new file mode 100755 index 000000000..a55b49a3a --- /dev/null +++ b/app/src/main/java/io/legado/app/model/CheckSource.kt @@ -0,0 +1,69 @@ +package io.legado.app.model + +import android.content.Context +import io.legado.app.R +import io.legado.app.constant.IntentAction +import io.legado.app.data.entities.BookSourcePart +import io.legado.app.help.CacheManager +import io.legado.app.help.IntentData +import io.legado.app.service.CheckSourceService +import io.legado.app.utils.startService +import splitties.init.appCtx + +object CheckSource { + var keyword = "我的" + + //校验设置 + var timeout = CacheManager.getLong("checkSourceTimeout") ?: 180000L + var checkSearch = CacheManager.get("checkSearch")?.toBoolean() ?: true + var checkDiscovery = CacheManager.get("checkDiscovery")?.toBoolean() ?: true + var checkInfo = CacheManager.get("checkInfo")?.toBoolean() ?: true + var checkCategory = CacheManager.get("checkCategory")?.toBoolean() ?: true + var checkContent = CacheManager.get("checkContent")?.toBoolean() ?: true + val summary get() = upSummary() + + fun start(context: Context, sources: List) { + val selectedIds = sources.map { + it.bookSourceUrl + } + IntentData.put("checkSourceSelectedIds", selectedIds) + context.startService { + action = IntentAction.start + } + } + + fun stop(context: Context) { + context.startService { + action = IntentAction.stop + } + } + + fun resume(context: Context) { + context.startService { + action = IntentAction.resume + } + } + + fun putConfig() { + CacheManager.put("checkSourceTimeout", timeout) + CacheManager.put("checkSearch", checkSearch) + CacheManager.put("checkDiscovery", checkDiscovery) + CacheManager.put("checkInfo", checkInfo) + CacheManager.put("checkCategory", checkCategory) + CacheManager.put("checkContent", checkContent) + } + + private fun upSummary(): String { + var checkItem = "" + if (checkSearch) checkItem = "$checkItem ${appCtx.getString(R.string.search)}" + if (checkDiscovery) checkItem = "$checkItem ${appCtx.getString(R.string.discovery)}" + if (checkInfo) checkItem = "$checkItem ${appCtx.getString(R.string.source_tab_info)}" + if (checkCategory) checkItem = "$checkItem ${appCtx.getString(R.string.chapter_list)}" + if (checkContent) checkItem = "$checkItem ${appCtx.getString(R.string.main_body)}" + return appCtx.getString( + R.string.check_source_config_summary, + (timeout / 1000).toString(), + checkItem + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/Debug.kt b/app/src/main/java/io/legado/app/model/Debug.kt new file mode 100755 index 000000000..a0748328a --- /dev/null +++ b/app/src/main/java/io/legado/app/model/Debug.kt @@ -0,0 +1,310 @@ +package io.legado.app.model + +import android.annotation.SuppressLint +import android.util.Log +import io.legado.app.BuildConfig +import io.legado.app.constant.AppPattern +import io.legado.app.data.entities.* +import io.legado.app.help.book.isWebFile +import io.legado.app.help.coroutine.CompositeCoroutine +import io.legado.app.help.source.sortUrls +import io.legado.app.model.rss.Rss +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.HtmlFormatter +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.stackTraceStr +import kotlinx.coroutines.CoroutineScope +import java.text.SimpleDateFormat +import java.util.* + +object Debug { + var callback: Callback? = null + private var debugSource: String? = null + private val tasks: CompositeCoroutine = CompositeCoroutine() + val debugMessageMap = HashMap() + private val debugTimeMap = HashMap() + var isChecking: Boolean = false + + @SuppressLint("ConstantLocale") + private val debugTimeFormat = SimpleDateFormat("[mm:ss.SSS]", Locale.getDefault()) + private var startTime: Long = System.currentTimeMillis() + + @Synchronized + fun log( + sourceUrl: String?, + msg: String = "", + print: Boolean = true, + isHtml: Boolean = false, + showTime: Boolean = true, + state: Int = 1 + ) { + if (BuildConfig.DEBUG) { + Log.d("sourceDebug", msg) + } + //调试信息始终要执行 + callback?.let { + if ((debugSource != sourceUrl || !print)) return + var printMsg = msg + if (isHtml) { + printMsg = HtmlFormatter.format(msg) + } + if (showTime) { + val time = debugTimeFormat.format(Date(System.currentTimeMillis() - startTime)) + printMsg = "$time $printMsg" + } + it.printLog(state, printMsg) + } + if (isChecking && sourceUrl != null && (msg).length < 30) { + var printMsg = msg + if (isHtml) { + printMsg = HtmlFormatter.format(msg) + } + if (showTime && debugTimeMap[sourceUrl] != null) { + val time = + debugTimeFormat.format(Date(System.currentTimeMillis() - debugTimeMap[sourceUrl]!!)) + printMsg = printMsg.replace(AppPattern.debugMessageSymbolRegex, "") + + debugMessageMap[sourceUrl] = "$time $printMsg" + } + } + } + + @Synchronized + fun log(msg: String?) { + log(debugSource, msg ?: "", true) + } + + fun cancelDebug(destroy: Boolean = false) { + tasks.clear() + + if (destroy) { + debugSource = null + callback = null + } + } + + fun startChecking(source: BookSource) { + isChecking = true + debugTimeMap[source.bookSourceUrl] = System.currentTimeMillis() + debugMessageMap[source.bookSourceUrl] = "${debugTimeFormat.format(Date(0))} 开始校验" + } + + fun finishChecking() { + isChecking = false + } + + fun getRespondTime(sourceUrl: String): Long { + return debugTimeMap[sourceUrl] ?: CheckSource.timeout + } + + fun updateFinalMessage(sourceUrl: String, state: String) { + if (debugTimeMap[sourceUrl] != null && debugMessageMap[sourceUrl] != null) { + val spendingTime = System.currentTimeMillis() - debugTimeMap[sourceUrl]!! + debugTimeMap[sourceUrl] = + if (state == "校验成功") spendingTime else CheckSource.timeout + spendingTime + val printTime = debugTimeFormat.format(Date(spendingTime)) + debugMessageMap[sourceUrl] = "$printTime $state" + } + } + + suspend fun startDebug(scope: CoroutineScope, rssSource: RssSource) { + cancelDebug() + debugSource = rssSource.sourceUrl + log(debugSource, "︾开始解析") + val sort = rssSource.sortUrls().first() + Rss.getArticles(scope, sort.first, sort.second, rssSource, 1) + .onSuccess { + if (it.first.isEmpty()) { + log(debugSource, "⇒列表页解析成功,为空") + log(debugSource, "︽解析完成", state = 1000) + } else { + val ruleContent = rssSource.ruleContent + if (!rssSource.ruleArticles.isNullOrBlank() && rssSource.ruleDescription.isNullOrBlank()) { + log(debugSource, "︽列表页解析完成") + log(debugSource, showTime = false) + if (ruleContent.isNullOrEmpty()) { + log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000) + } else { + rssContentDebug(scope, it.first[0], ruleContent, rssSource) + } + } else { + log(debugSource, "⇒存在描述规则,不解析内容页") + log(debugSource, "︽解析完成", state = 1000) + } + } + } + .onError { + log(debugSource, it.stackTraceStr, state = -1) + } + } + + private fun rssContentDebug( + scope: CoroutineScope, + rssArticle: RssArticle, + ruleContent: String, + rssSource: RssSource + ) { + log(debugSource, "︾开始解析内容页") + Rss.getContent(scope, rssArticle, ruleContent, rssSource) + .onSuccess { + log(debugSource, it) + log(debugSource, "︽内容页解析完成", state = 1000) + } + .onError { + log(debugSource, it.stackTraceStr, state = -1) + } + } + + fun startDebug(scope: CoroutineScope, bookSource: BookSource, key: String) { + cancelDebug() + debugSource = bookSource.bookSourceUrl + startTime = System.currentTimeMillis() + when { + key.isAbsUrl() -> { + val book = Book() + book.origin = bookSource.bookSourceUrl + book.bookUrl = key + log(bookSource.bookSourceUrl, "⇒开始访问详情页:$key") + infoDebug(scope, bookSource, book) + } + + key.contains("::") -> { + val url = key.substringAfter("::") + log(bookSource.bookSourceUrl, "⇒开始访问发现页:$url") + exploreDebug(scope, bookSource, url) + } + + key.startsWith("++") -> { + val url = key.substring(2) + val book = Book() + book.origin = bookSource.bookSourceUrl + book.tocUrl = url + log(bookSource.bookSourceUrl, "⇒开始访目录页:$url") + tocDebug(scope, bookSource, book) + } + + key.startsWith("--") -> { + val url = key.substring(2) + val book = Book() + book.origin = bookSource.bookSourceUrl + log(bookSource.bookSourceUrl, "⇒开始访正文页:$url") + val chapter = BookChapter() + chapter.title = "调试" + chapter.url = url + contentDebug(scope, bookSource, book, chapter, null) + } + + else -> { + log(bookSource.bookSourceUrl, "⇒开始搜索关键字:$key") + searchDebug(scope, bookSource, key) + } + } + } + + private fun exploreDebug(scope: CoroutineScope, bookSource: BookSource, url: String) { + log(debugSource, "︾开始解析发现页") + val explore = WebBook.exploreBook(scope, bookSource, url, 1) + .onSuccess { exploreBooks -> + if (exploreBooks.isNotEmpty()) { + log(debugSource, "︽发现页解析完成") + log(debugSource, showTime = false) + infoDebug(scope, bookSource, exploreBooks[0].toBook()) + } else { + log(debugSource, "︽未获取到书籍", state = -1) + } + } + .onError { + log(debugSource, it.stackTraceStr, state = -1) + } + tasks.add(explore) + } + + private fun searchDebug(scope: CoroutineScope, bookSource: BookSource, key: String) { + log(debugSource, "︾开始解析搜索页") + val search = WebBook.searchBook(scope, bookSource, key, 1) + .onSuccess { searchBooks -> + if (searchBooks.isNotEmpty()) { + log(debugSource, "︽搜索页解析完成") + log(debugSource, showTime = false) + infoDebug(scope, bookSource, searchBooks[0].toBook()) + } else { + log(debugSource, "︽未获取到书籍", state = -1) + } + } + .onError { + log(debugSource, it.stackTraceStr, state = -1) + } + tasks.add(search) + } + + private fun infoDebug(scope: CoroutineScope, bookSource: BookSource, book: Book) { + if (book.tocUrl.isNotBlank()) { + log(debugSource, "≡已获取目录链接,跳过详情页") + log(debugSource, showTime = false) + tocDebug(scope, bookSource, book) + return + } + log(debugSource, "︾开始解析详情页") + val info = WebBook.getBookInfo(scope, bookSource, book) + .onSuccess { + log(debugSource, "︽详情页解析完成") + log(debugSource, showTime = false) + if (!book.isWebFile) { + tocDebug(scope, bookSource, book) + } else { + log(debugSource, "≡文件类书源跳过解析目录", state = 1000) + } + } + .onError { + log(debugSource, it.stackTraceStr, state = -1) + } + tasks.add(info) + } + + private fun tocDebug(scope: CoroutineScope, bookSource: BookSource, book: Book) { + log(debugSource, "︾开始解析目录页") + val chapterList = WebBook.getChapterList(scope, bookSource, book) + .onSuccess { chapters -> + log(debugSource, "︽目录页解析完成") + log(debugSource, showTime = false) + val toc = chapters.filter { !(it.isVolume && it.url.startsWith(it.title)) } + if (toc.isEmpty()) { + log(debugSource, "≡没有正文章节") + return@onSuccess + } + val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url + contentDebug(scope, bookSource, book, toc.first(), nextChapterUrl) + } + .onError { + log(debugSource, it.stackTraceStr, state = -1) + } + tasks.add(chapterList) + } + + private fun contentDebug( + scope: CoroutineScope, + bookSource: BookSource, + book: Book, + bookChapter: BookChapter, + nextChapterUrl: String? + ) { + log(debugSource, "︾开始解析正文页") + val content = WebBook.getContent( + scope = scope, + bookSource = bookSource, + book = book, + bookChapter = bookChapter, + nextChapterUrl = nextChapterUrl, + needSave = false + ).onSuccess { + log(debugSource, "︽正文页解析完成", state = 1000) + }.onError { + log(debugSource, it.stackTraceStr, state = -1) + } + tasks.add(content) + } + + interface Callback { + fun printLog(state: Int, msg: String) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/Download.kt b/app/src/main/java/io/legado/app/model/Download.kt new file mode 100755 index 000000000..5df091448 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/Download.kt @@ -0,0 +1,19 @@ +package io.legado.app.model + +import android.content.Context +import io.legado.app.constant.IntentAction +import io.legado.app.service.DownloadService +import io.legado.app.utils.startService + +object Download { + + + fun start(context: Context, url: String, fileName: String) { + context.startService { + action = IntentAction.start + putExtra("url", url) + putExtra("fileName", fileName) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/ImageProvider.kt b/app/src/main/java/io/legado/app/model/ImageProvider.kt new file mode 100755 index 000000000..dfc035064 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/ImageProvider.kt @@ -0,0 +1,212 @@ +package io.legado.app.model + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Size +import androidx.collection.LruCache +import io.legado.app.R +import io.legado.app.constant.AppLog.putDebug +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isEpub +import io.legado.app.help.book.isMobi +import io.legado.app.help.book.isPdf +import io.legado.app.help.config.AppConfig +import io.legado.app.model.localBook.EpubFile +import io.legado.app.model.localBook.MobiFile +import io.legado.app.model.localBook.PdfFile +import io.legado.app.utils.BitmapUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.SvgUtils +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.io.File +import java.io.FileOutputStream +import kotlin.math.min + +object ImageProvider { + + private val errorBitmap: Bitmap by lazy { + BitmapFactory.decodeResource(appCtx.resources, R.drawable.image_loading_error) + } + + /** + * 缓存bitmap LruCache实现 + * filePath bitmap + */ + private const val M = 1024 * 1024 + val cacheSize: Int + get() { + if (AppConfig.bitmapCacheSize !in 1..1024) { + AppConfig.bitmapCacheSize = 50 + } + return AppConfig.bitmapCacheSize * M + } + + val bitmapLruCache = BitmapLruCache() + + class BitmapLruCache : LruCache(cacheSize) { + + private var removeCount = 0 + + val count get() = putCount() + createCount() - evictionCount() - removeCount + + override fun sizeOf(key: String, value: Bitmap): Int { + return value.byteCount + } + + override fun entryRemoved( + evicted: Boolean, + key: String, + oldValue: Bitmap, + newValue: Bitmap? + ) { + if (!evicted) { + synchronized(this) { + removeCount++ + } + } + //错误图片不能释放,占位用,防止一直重复获取图片 + if (oldValue != errorBitmap) { + oldValue.recycle() + //putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath") + //putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes") + } + } + + } + + fun put(key: String, bitmap: Bitmap) { + ensureLruCacheSize(bitmap) + bitmapLruCache.put(key, bitmap) + } + + fun get(key: String): Bitmap? { + return bitmapLruCache[key] + } + + fun remove(key: String): Bitmap? { + return bitmapLruCache.remove(key) + } + + private fun getNotRecycled(key: String): Bitmap? { + val bitmap = bitmapLruCache[key] ?: return null + if (bitmap.isRecycled) { + bitmapLruCache.remove(key) + return null + } + return bitmap + } + + private fun ensureLruCacheSize(bitmap: Bitmap) { + val lruMaxSize = bitmapLruCache.maxSize() + val lruSize = bitmapLruCache.size() + val byteCount = bitmap.byteCount + val size = if (byteCount > lruMaxSize) { + min(256 * M, (byteCount * 1.3).toInt()) + } else if (lruSize + byteCount > lruMaxSize && bitmapLruCache.count < 5) { + min(256 * M, (lruSize + byteCount * 1.3).toInt()) + } else { + lruMaxSize + } + if (size > lruMaxSize) { + bitmapLruCache.resize(size) + } + } + + /** + *缓存网络图片和epub图片 + */ + suspend fun cacheImage( + book: Book, + src: String, + bookSource: BookSource? + ): File { + return withContext(IO) { + val vFile = BookHelp.getImage(book, src) + if (!BookHelp.isImageExist(book, src)) { + val inputStream = when { + book.isEpub -> EpubFile.getImage(book, src) + book.isPdf -> PdfFile.getImage(book, src) + book.isMobi -> MobiFile.getImage(book, src) + else -> { + BookHelp.saveImage(bookSource, book, src) + null + } + } + inputStream?.use { input -> + val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath) + FileOutputStream(newFile).use { output -> + input.copyTo(output) + } + } + } + return@withContext vFile + } + } + + /** + *获取图片宽度高度信息 + */ + suspend fun getImageSize( + book: Book, + src: String, + bookSource: BookSource? + ): Size { + val file = cacheImage(book, src, bookSource) + val op = BitmapFactory.Options() + // inJustDecodeBounds如果设置为true,仅仅返回图片实际的宽和高,宽和高是赋值给opts.outWidth,opts.outHeight; + op.inJustDecodeBounds = true + BitmapFactory.decodeFile(file.absolutePath, op) + if (op.outWidth < 1 && op.outHeight < 1) { + //svg size + val size = SvgUtils.getSize(file.absolutePath) + if (size != null) return size + putDebug("ImageProvider: $src Unsupported image type") + //file.delete() 重复下载 + return Size(errorBitmap.width, errorBitmap.height) + } + return Size(op.outWidth, op.outHeight) + } + + /** + *获取bitmap 使用LruCache缓存 + */ + fun getImage( + book: Book, + src: String, + width: Int, + height: Int? = null + ): Bitmap { + //src为空白时 可能被净化替换掉了 或者规则失效 + if (book.getUseReplaceRule() && src.isBlank()) { + book.setUseReplaceRule(false) + appCtx.toastOnUi(R.string.error_image_url_empty) + } + val vFile = BookHelp.getImage(book, src) + if (!vFile.exists()) return errorBitmap + //epub文件提供图片链接是相对链接,同时阅读多个epub文件,缓存命中错误 + //bitmapLruCache的key同一改成缓存文件的路径 + val cacheBitmap = getNotRecycled(vFile.absolutePath) + if (cacheBitmap != null) return cacheBitmap + return kotlin.runCatching { + val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height) + ?: SvgUtils.createBitmap(vFile.absolutePath, width, height) + ?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap)) + put(vFile.absolutePath, bitmap) + bitmap + }.onFailure { + //错误图片占位,防止重复获取 + put(vFile.absolutePath, errorBitmap) + }.getOrDefault(errorBitmap) + } + + fun clear() { + bitmapLruCache.evictAll() + } + +} diff --git a/app/src/main/java/io/legado/app/model/README.md b/app/src/main/java/io/legado/app/model/README.md new file mode 100755 index 000000000..8583fd4bb --- /dev/null +++ b/app/src/main/java/io/legado/app/model/README.md @@ -0,0 +1,7 @@ +# 放置一些模块类 + +* analyzeRule 书源规则解析 +* localBook 本地书籍解析 +* rss 订阅规则解析 +* webBook 获取网络书籍 + diff --git a/app/src/main/java/io/legado/app/model/ReadAloud.kt b/app/src/main/java/io/legado/app/model/ReadAloud.kt new file mode 100755 index 000000000..8c1a61398 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/ReadAloud.kt @@ -0,0 +1,137 @@ +package io.legado.app.model + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +import io.legado.app.data.appDb +import io.legado.app.data.entities.HttpTTS +import io.legado.app.help.config.AppConfig +import io.legado.app.service.BaseReadAloudService +import io.legado.app.service.HttpReadAloudService +import io.legado.app.service.TTSReadAloudService +import io.legado.app.utils.LogUtils +import io.legado.app.utils.StringUtils +import io.legado.app.utils.postEvent +import io.legado.app.utils.startForegroundServiceCompat +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx + +object ReadAloud { + private var aloudClass: Class<*> = getReadAloudClass() + val ttsEngine get() = ReadBook.book?.getTtsEngine() ?: AppConfig.ttsEngine + var httpTTS: HttpTTS? = null + + private fun getReadAloudClass(): Class<*> { + val ttsEngine = ttsEngine + if (ttsEngine.isNullOrBlank()) { + return TTSReadAloudService::class.java + } + if (StringUtils.isNumeric(ttsEngine)) { + httpTTS = appDb.httpTTSDao.get(ttsEngine.toLong()) + if (httpTTS != null) { + return HttpReadAloudService::class.java + } + } + return TTSReadAloudService::class.java + } + + fun upReadAloudClass() { + stop(appCtx) + aloudClass = getReadAloudClass() + } + + fun play( + context: Context, + play: Boolean = true, + pageIndex: Int = ReadBook.durPageIndex, + startPos: Int = 0 + ) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.play + intent.putExtra("play", play) + intent.putExtra("pageIndex", pageIndex) + intent.putExtra("startPos", startPos) + LogUtils.d("ReadAloud", intent.toString()) + try { + context.startForegroundServiceCompat(intent) + } catch (e: Exception) { + val msg = "启动朗读服务出错\n${e.localizedMessage}" + AppLog.put(msg, e) + context.toastOnUi(msg) + } + } + + fun playByEventBus( + play: Boolean = true, + pageIndex: Int = ReadBook.durPageIndex, + startPos: Int = 0 + ) { + val bundle = Bundle().apply { + putBoolean("play", play) + putInt("pageIndex", pageIndex) + putInt("startPos", startPos) + } + postEvent(EventBus.READ_ALOUD_PLAY, bundle) + } + + fun pause(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.pause + context.startForegroundServiceCompat(intent) + } + } + + fun resume(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.resume + context.startForegroundServiceCompat(intent) + } + } + + fun stop(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.stop + context.startForegroundServiceCompat(intent) + } + } + + fun prevParagraph(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.prevParagraph + context.startForegroundServiceCompat(intent) + } + } + + fun nextParagraph(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.nextParagraph + context.startForegroundServiceCompat(intent) + } + } + + fun upTtsSpeechRate(context: Context) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.upTtsSpeechRate + context.startForegroundServiceCompat(intent) + } + } + + fun setTimer(context: Context, minute: Int) { + if (BaseReadAloudService.isRun) { + val intent = Intent(context, aloudClass) + intent.action = IntentAction.setTimer + intent.putExtra("minute", minute) + context.startForegroundServiceCompat(intent) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/ReadBook.kt b/app/src/main/java/io/legado/app/model/ReadBook.kt new file mode 100755 index 000000000..a20ae7ffe --- /dev/null +++ b/app/src/main/java/io/legado/app/model/ReadBook.kt @@ -0,0 +1,1054 @@ +package io.legado.app.model + +import io.legado.app.constant.AppLog +import io.legado.app.constant.EventBus +import io.legado.app.constant.PageAnim.scrollPageAnim +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.ReadRecord +import io.legado.app.help.AppWebDav +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isImage +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isPdf +import io.legado.app.help.book.isSameNameAuthor +import io.legado.app.help.book.readSimulating +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.book.update +import io.legado.app.help.config.AppConfig +import io.legado.app.help.config.ReadBookConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.globalExecutor +import io.legado.app.model.localBook.TextFile +import io.legado.app.model.webBook.WebBook +import io.legado.app.service.BaseReadAloudService +import io.legado.app.service.CacheBookService +import io.legado.app.ui.book.read.page.entities.TextChapter +import io.legado.app.ui.book.read.page.provider.ChapterProvider +import io.legado.app.ui.book.read.page.provider.LayoutProgressListener +import io.legado.app.utils.postEvent +import io.legado.app.utils.stackTraceStr +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import splitties.init.appCtx +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.max +import kotlin.math.min + + +@Suppress("MemberVisibilityCanBePrivate") +object ReadBook : CoroutineScope by MainScope() { + var book: Book? = null + var callBack: CallBack? = null + var inBookshelf = false + var chapterSize = 0 + var simulatedChapterSize = 0 + var durChapterIndex = 0 + var durChapterPos = 0 + var isLocalBook = true + var chapterChanged = false + var prevTextChapter: TextChapter? = null + var curTextChapter: TextChapter? = null + var nextTextChapter: TextChapter? = null + var bookSource: BookSource? = null + var msg: String? = null + private val loadingChapters = arrayListOf() + private val readRecord = ReadRecord() + private val chapterLoadingJobs = ConcurrentHashMap>() + private val prevChapterLoadingLock = Mutex() + private val curChapterLoadingLock = Mutex() + private val nextChapterLoadingLock = Mutex() + var readStartTime: Long = System.currentTimeMillis() + + /* 跳转进度前进度记录 */ + var lastBookProgress: BookProgress? = null + + /* web端阅读进度记录 */ + var webBookProgress: BookProgress? = null + + var preDownloadTask: Job? = null + val downloadedChapters = hashSetOf() + val downloadFailChapters = hashMapOf() + var contentProcessor: ContentProcessor? = null + val downloadScope = CoroutineScope(SupervisorJob() + IO) + val preDownloadSemaphore = Semaphore(2) + val executor = globalExecutor + + fun resetData(book: Book) { + releaseAndCancel() + ReadBook.book = book + readRecord.bookName = book.name + readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0 + chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) + simulatedChapterSize = if (book.readSimulating()) { + book.simulatedTotalChapterNum() + } else { + chapterSize + } + contentProcessor = ContentProcessor.get(book) + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + isLocalBook = book.isLocal + clearTextChapter() + callBack?.upContent() + callBack?.upMenuView() + callBack?.upPageAnim() + upWebBook(book) + lastBookProgress = null + webBookProgress = null + TextFile.clear() + synchronized(this) { + loadingChapters.clear() + downloadedChapters.clear() + downloadFailChapters.clear() + } + } + + fun upData(book: Book) { + releaseAndCancel() + ReadBook.book = book + chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) + simulatedChapterSize = if (book.readSimulating()) { + book.simulatedTotalChapterNum() + } else { + chapterSize + } + if (durChapterIndex != book.durChapterIndex) { + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + clearTextChapter() + } + if (curTextChapter?.isCompleted == false) { + curTextChapter = null + } + if (nextTextChapter?.isCompleted == false) { + nextTextChapter = null + } + if (prevTextChapter?.isCompleted == false) { + prevTextChapter = null + } + callBack?.upMenuView() + upWebBook(book) + synchronized(this) { + loadingChapters.clear() + downloadedChapters.clear() + downloadFailChapters.clear() + } + } + + fun upWebBook(book: Book) { + if (book.isLocal) { + bookSource = null + if (book.getImageStyle().isNullOrBlank() && (book.isImage || book.isPdf)) { + book.setImageStyle(Book.imgStyleFull) + } + } else { + appDb.bookSourceDao.getBookSource(book.origin)?.let { + bookSource = it + if (book.getImageStyle().isNullOrBlank()) { + var imageStyle = it.getContentRule().imageStyle + if (imageStyle.isNullOrBlank() && (book.isImage || book.isPdf)) { + imageStyle = Book.imgStyleFull + } + book.setImageStyle(imageStyle) + if (imageStyle.equals(Book.imgStyleSingle, true)) { + book.setPageAnim(0) + } + } + } ?: let { + bookSource = null + } + } + } + + fun upReadBookConfig(book: Book) { + val oldIndex = ReadBookConfig.styleSelect + ReadBookConfig.isComic = book.isImage + if (oldIndex != ReadBookConfig.styleSelect) { + postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) + if (AppConfig.readBarStyleFollowPage) { + postEvent(EventBus.UPDATE_READ_ACTION_BAR, true) + } + } + } + + fun setProgress(progress: BookProgress) { + if (progress.durChapterIndex < chapterSize && + (durChapterIndex != progress.durChapterIndex + || durChapterPos != progress.durChapterPos) + ) { + durChapterIndex = progress.durChapterIndex + durChapterPos = progress.durChapterPos + saveRead() + clearTextChapter() + callBack?.upContent() + loadContent(resetPageOffset = true) + } + } + + //暂时保存跳转前进度 + fun saveCurrentBookProgress() { + if (lastBookProgress != null) return //避免进度条连续跳转不能覆盖最初的进度记录 + lastBookProgress = book?.let { BookProgress(it) } + } + + //恢复跳转前进度 + fun restoreLastBookProgress() { + lastBookProgress?.let { + setProgress(it) + lastBookProgress = null + } + } + + fun clearTextChapter() { + clearExpiredChapterLoadingJob(true) + prevTextChapter = null + curTextChapter = null + nextTextChapter = null + } + + fun clearSearchResult() { + curTextChapter?.clearSearchResult() + prevTextChapter?.clearSearchResult() + nextTextChapter?.clearSearchResult() + } + + fun uploadProgress(toast: Boolean = false, successAction: (() -> Unit)? = null) { + book?.let { + launch(IO) { + AppWebDav.uploadBookProgress(it, toast) { + successAction?.invoke() + } + ensureActive() + it.update() + } + } + } + + /** + * 同步阅读进度 + * 如果当前进度快于服务器进度或者没有进度进行上传,如果慢与服务器进度则执行传入动作 + */ + fun syncProgress( + newProgressAction: ((progress: BookProgress) -> Unit)? = null, + uploadSuccessAction: (() -> Unit)? = null, + syncSuccessAction: (() -> Unit)? = null + ) { + if (!AppConfig.syncBookProgress) return + val book = book ?: return + Coroutine.async { + AppWebDav.getBookProgress(book) + }.onError { + AppLog.put("拉取阅读进度失败", it) + }.onSuccess { progress -> + if (progress == null || progress.durChapterIndex < book.durChapterIndex || + (progress.durChapterIndex == book.durChapterIndex + && progress.durChapterPos < book.durChapterPos) + ) { + // 服务器没有进度或者进度比服务器快,上传现有进度 + Coroutine.async { + AppWebDav.uploadBookProgress(BookProgress(book), uploadSuccessAction) + book.update() + } + } else if (progress.durChapterIndex > book.durChapterIndex || + progress.durChapterPos > book.durChapterPos + ) { + // 进度比服务器慢,执行传入动作 + newProgressAction?.invoke(progress) + } else { + syncSuccessAction?.invoke() + } + } + } + + fun upReadTime() { + executor.execute { + if (!AppConfig.enableReadRecord) { + return@execute + } + readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime + readStartTime = System.currentTimeMillis() + readRecord.lastRead = System.currentTimeMillis() + appDb.readRecordDao.insert(readRecord) + } + } + + fun upMsg(msg: String?) { + if (ReadBook.msg != msg) { + ReadBook.msg = msg + callBack?.upContent() + } + } + + fun moveToNextPage(): Boolean { + var hasNextPage = false + curTextChapter?.let { + val nextPagePos = it.getNextPageLength(durChapterPos) + if (nextPagePos >= 0) { + hasNextPage = true + it.getPage(durPageIndex)?.removePageAloudSpan() + durChapterPos = nextPagePos + callBack?.cancelSelect() + callBack?.upContent() + saveRead(true) + } + } + return hasNextPage + } + + fun moveToPrevPage(): Boolean { + var hasPrevPage = false + curTextChapter?.let { + val prevPagePos = it.getPrevPageLength(durChapterPos) + if (prevPagePos >= 0) { + hasPrevPage = true + durChapterPos = prevPagePos + callBack?.upContent() + saveRead(true) + } + } + return hasPrevPage + } + + fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean { + if (durChapterIndex < simulatedChapterSize - 1) { + durChapterPos = 0 + durChapterIndex++ + clearExpiredChapterLoadingJob() + prevTextChapter = curTextChapter + curTextChapter = nextTextChapter + nextTextChapter = null + if (curTextChapter == null) { + AppLog.putDebug("moveToNextChapter-章节未加载,开始加载") + if (upContentInPlace) callBack?.upContent() + loadContent(durChapterIndex, upContent, resetPageOffset = false) + } else if (upContent && upContentInPlace) { + AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图") + callBack?.upContent() + } + loadContent(durChapterIndex.plus(1), upContent, false) + saveRead() + callBack?.upMenuView() + AppLog.putDebug("moveToNextChapter-curPageChanged()") + curPageChanged() + return true + } else { + AppLog.putDebug("跳转下一章失败,没有下一章") + return false + } + } + + suspend fun moveToNextChapterAwait( + upContent: Boolean, + upContentInPlace: Boolean = true + ): Boolean { + if (durChapterIndex < simulatedChapterSize - 1) { + durChapterPos = 0 + durChapterIndex++ + clearExpiredChapterLoadingJob() + prevTextChapter = curTextChapter + curTextChapter = nextTextChapter + nextTextChapter = null + if (curTextChapter == null) { + AppLog.putDebug("moveToNextChapter-章节未加载,开始加载") + if (upContentInPlace) callBack?.upContentAwait() + loadContentAwait(durChapterIndex, upContent, resetPageOffset = false) + } else if (upContent && upContentInPlace) { + AppLog.putDebug("moveToNextChapter-章节已加载,刷新视图") + callBack?.upContentAwait() + } + loadContent(durChapterIndex.plus(1), upContent, false) + saveRead() + callBack?.upMenuView() + AppLog.putDebug("moveToNextChapter-curPageChanged()") + curPageChanged() + return true + } else { + AppLog.putDebug("跳转下一章失败,没有下一章") + return false + } + } + + fun moveToPrevChapter( + upContent: Boolean, + toLast: Boolean = true, + upContentInPlace: Boolean = true + ): Boolean { + if (durChapterIndex > 0) { + durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0 + durChapterIndex-- + clearExpiredChapterLoadingJob() + nextTextChapter = curTextChapter + curTextChapter = prevTextChapter + prevTextChapter = null + if (curTextChapter == null) { + if (upContentInPlace) callBack?.upContent() + loadContent(durChapterIndex, upContent, resetPageOffset = false) + } else if (upContent && upContentInPlace) { + callBack?.upContent() + } + loadContent(durChapterIndex.minus(1), upContent, false) + saveRead() + callBack?.upMenuView() + curPageChanged() + return true + } else { + return false + } + } + + fun skipToPage(index: Int, success: (() -> Unit)? = null) { + durChapterPos = curTextChapter?.getReadLength(index) ?: index + callBack?.upContent { + success?.invoke() + } + curPageChanged() + saveRead(true) + } + + fun setPageIndex(index: Int) { + recycleRecorders(durPageIndex, index) + durChapterPos = curTextChapter?.getReadLength(index) ?: index + saveRead(true) + curPageChanged(true) + } + + fun recycleRecorders(beforeIndex: Int, afterIndex: Int) { + if (!AppConfig.optimizeRender) { + return + } + executor.execute { + val textChapter = curTextChapter ?: return@execute + if (afterIndex > beforeIndex) { + textChapter.getPage(afterIndex - 2)?.recycleRecorders() + } + if (afterIndex < beforeIndex) { + textChapter.getPage(afterIndex + 3)?.recycleRecorders() + } + } + } + + fun openChapter( + index: Int, + durChapterPos: Int = 0, + upContent: Boolean = true, + success: (() -> Unit)? = null + ) { + if (index < chapterSize) { + clearTextChapter() + if (upContent) callBack?.upContent() + durChapterIndex = index + ReadBook.durChapterPos = durChapterPos + saveRead() + loadContent(resetPageOffset = true) { + success?.invoke() + } + } + } + + /** + * 当前页面变化 + */ + private fun curPageChanged(pageChanged: Boolean = false) { + callBack?.pageChanged() + curTextChapter?.let { + if (BaseReadAloudService.isRun && it.isCompleted) { + val scrollPageAnim = pageAnim() == 3 + if (scrollPageAnim && pageChanged) { + ReadAloud.pause(appCtx) + } else { + readAloud(!BaseReadAloudService.pause) + } + } + } + upReadTime() + preDownload() + } + + /** + * 朗读 + */ + fun readAloud(play: Boolean = true, startPos: Int = 0) { + book ?: return + val textChapter = curTextChapter ?: return + if (textChapter.isCompleted) { + ReadAloud.play(appCtx, play, startPos = startPos) + } + } + + /** + * 当前页数 + */ + val durPageIndex: Int + get() { + return curTextChapter?.getPageIndexByCharIndex(durChapterPos) ?: durChapterPos + } + + /** + * 是否排版到了当前阅读位置 + */ + val isLayoutAvailable inline get() = durPageIndex >= 0 + + val isScroll inline get() = pageAnim() == scrollPageAnim + + val contentLoadFinish get() = curTextChapter != null || msg != null + + /** + * chapterOnDur: 0为当前页,1为下一页,-1为上一页 + */ + fun textChapter(chapterOnDur: Int = 0): TextChapter? { + return when (chapterOnDur) { + 0 -> curTextChapter + 1 -> nextTextChapter + -1 -> prevTextChapter + else -> null + } + } + + /** + * 加载当前章节和前后一章内容 + * @param resetPageOffset 滚动阅读是否重置滚动位置 + * @param success 当前章节加载完成回调 + */ + fun loadContent( + resetPageOffset: Boolean, + success: (() -> Unit)? = null + ) { + loadContent(durChapterIndex, resetPageOffset = resetPageOffset) { + success?.invoke() + } + loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset) + loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset) + } + + fun loadOrUpContent() { + if (curTextChapter == null) { + loadContent(durChapterIndex) + } else { + callBack?.upContent() + } + if (nextTextChapter == null) { + loadContent(durChapterIndex + 1) + } + if (prevTextChapter == null) { + loadContent(durChapterIndex - 1) + } + } + + /** + * 加载章节内容 + * @param index 章节序号 + * @param upContent 是否更新视图 + * @param resetPageOffset 滚动阅读是否重置滚动位置 + * @param success 加载完成回调 + */ + fun loadContent( + index: Int, + upContent: Boolean = true, + resetPageOffset: Boolean = false, + success: (() -> Unit)? = null + ) { + Coroutine.async { + val book = book!! + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return@async + if (addLoading(index)) { + BookHelp.getContent(book, chapter)?.let { + contentLoadFinish( + book, + chapter, + it, + upContent, + resetPageOffset, + success = success + ) + } ?: download( + downloadScope, + chapter, + resetPageOffset + ) + } + }.onError { + AppLog.put("加载正文出错\n${it.localizedMessage}") + } + } + + suspend fun loadContentAwait( + index: Int, + upContent: Boolean = true, + resetPageOffset: Boolean = false, + success: (() -> Unit)? = null + ) = withContext(IO) { + if (addLoading(index)) { + try { + val book = book!! + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index)!! + val content = BookHelp.getContent(book, chapter) ?: downloadAwait(chapter) + contentLoadFinishAwait(book, chapter, content, upContent, resetPageOffset) + success?.invoke() + } catch (e: Exception) { + AppLog.put("加载正文出错\n${e.localizedMessage}") + } finally { + removeLoading(index) + } + } + } + + /** + * 下载正文 + */ + private suspend fun downloadIndex(index: Int) { + if (index < 0) return + if (index > chapterSize - 1) { + upToc() + return + } + val book = book ?: return + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return + if (BookHelp.hasContent(book, chapter)) { + downloadedChapters.add(chapter.index) + } else { + delay(1000) + if (addLoading(index)) { + download(downloadScope, chapter, false, preDownloadSemaphore) + } + } + } + + /** + * 下载正文 + */ + private fun download( + scope: CoroutineScope, + chapter: BookChapter, + resetPageOffset: Boolean, + semaphore: Semaphore? = null, + success: (() -> Unit)? = null + ) { + val book = book ?: return removeLoading(chapter.index) + val bookSource = bookSource + if (bookSource != null) { + CacheBook.getOrCreate(bookSource, book).download(scope, chapter, semaphore) + } else { + val msg = if (book.isLocal) "无内容" else "没有书源" + contentLoadFinish( + book, + chapter, + "加载正文失败\n$msg", + resetPageOffset = resetPageOffset, + success = success + ) + } + } + + private suspend fun downloadAwait(chapter: BookChapter): String { + val book = book!! + val bookSource = bookSource + if (bookSource != null) { + return CacheBook.getOrCreate(bookSource, book).downloadAwait(chapter) + } else { + val msg = if (book.isLocal) "无内容" else "没有书源" + return "加载正文失败\n$msg" + } + } + + @Synchronized + private fun addLoading(index: Int): Boolean { + if (loadingChapters.contains(index)) return false + loadingChapters.add(index) + return true + } + + @Synchronized + fun removeLoading(index: Int) { + loadingChapters.remove(index) + } + + /** + * 内容加载完成 + */ + @Synchronized + fun contentLoadFinish( + book: Book, + chapter: BookChapter, + content: String, + upContent: Boolean = true, + resetPageOffset: Boolean, + canceled: Boolean = false, + success: (() -> Unit)? = null + ) { + removeLoading(chapter.index) + if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) { + return + } + chapterLoadingJobs[chapter.index]?.cancel() + val job = Coroutine.async(this, start = CoroutineStart.LAZY) { + val contentProcessor = ContentProcessor.get(book.name, book.origin) + val displayTitle = chapter.getDisplayTitle( + contentProcessor.getTitleReplaceRules(), + book.getUseReplaceRule() + ) + val contents = contentProcessor + .getContent(book, chapter, content, includeTitle = false) + ensureActive() + val textChapter = ChapterProvider.getTextChapterAsync( + this, book, chapter, displayTitle, contents, simulatedChapterSize + ) + when (val offset = chapter.index - durChapterIndex) { + 0 -> curChapterLoadingLock.withLock { + withContext(Main) { + ensureActive() + curTextChapter = textChapter + } + callBack?.upMenuView() + var available = false + for (page in textChapter.layoutChannel) { + val index = page.index + if (!available && page.containPos(durChapterPos)) { + if (upContent) { + callBack?.upContent(offset, resetPageOffset) + } + available = true + } + if (upContent && isScroll) { + if (max(index - 3, 0) < durPageIndex) { + callBack?.upContent(offset, false) + } + } + callBack?.onLayoutPageCompleted(index, page) + } + if (upContent) callBack?.upContent(offset, !available && resetPageOffset) + curPageChanged() + callBack?.contentLoadFinish() + } + + -1 -> prevChapterLoadingLock.withLock { + withContext(Main) { + ensureActive() + prevTextChapter = textChapter + } + textChapter.layoutChannel.receiveAsFlow().collect() + if (upContent) callBack?.upContent(offset, resetPageOffset) + } + + 1 -> nextChapterLoadingLock.withLock { + withContext(Main) { + ensureActive() + nextTextChapter = textChapter + } + for (page in textChapter.layoutChannel) { + if (page.index > 1) { + continue + } + if (upContent) callBack?.upContent(offset, resetPageOffset) + } + } + } + + return@async + }.onError { + if (it is CancellationException) { + return@onError + } + AppLog.put("ChapterProvider ERROR", it) + appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}") + }.onSuccess { + success?.invoke() + } + chapterLoadingJobs[chapter.index] = job + job.start() + } + + suspend fun contentLoadFinishAwait( + book: Book, + chapter: BookChapter, + content: String, + upContent: Boolean = true, + resetPageOffset: Boolean + ) { + removeLoading(chapter.index) + if (chapter.index !in durChapterIndex - 1..durChapterIndex + 1) { + return + } + kotlin.runCatching { + val contentProcessor = ContentProcessor.get(book.name, book.origin) + val displayTitle = chapter.getDisplayTitle( + contentProcessor.getTitleReplaceRules(), + book.getUseReplaceRule() + ) + val contents = contentProcessor + .getContent(book, chapter, content, includeTitle = false) + val textChapter = ChapterProvider.getTextChapterAsync( + this@ReadBook, book, chapter, displayTitle, contents, simulatedChapterSize + ) + when (val offset = chapter.index - durChapterIndex) { + 0 -> { + curTextChapter?.cancelLayout() + withContext(Main) { + curTextChapter = textChapter + } + callBack?.upMenuView() + var available = false + for (page in textChapter.layoutChannel) { + val index = page.index + if (!available && page.containPos(durChapterPos)) { + if (upContent) { + callBack?.upContent(offset, resetPageOffset) + } + available = true + } + if (upContent && isScroll) { + if (max(index - 3, 0) < durPageIndex) { + callBack?.upContent(offset, false) + } + } + callBack?.onLayoutPageCompleted(index, page) + } + if (upContent) callBack?.upContent(offset, !available && resetPageOffset) + curPageChanged() + callBack?.contentLoadFinish() + } + + -1 -> { + prevTextChapter?.cancelLayout() + withContext(Main) { + prevTextChapter = textChapter + } + textChapter.layoutChannel.receiveAsFlow().collect() + if (upContent) callBack?.upContent(offset, resetPageOffset) + } + + 1 -> { + nextTextChapter?.cancelLayout() + withContext(Main) { + nextTextChapter = textChapter + } + for (page in textChapter.layoutChannel) { + if (page.index > 1) { + continue + } + if (upContent) callBack?.upContent(offset, resetPageOffset) + } + } + } + }.onFailure { + if (it is CancellationException) { + return@onFailure + } + AppLog.put("ChapterProvider ERROR", it) + appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}") + } + } + + @Synchronized + fun upToc() { + val bookSource = bookSource ?: return + val book = book ?: return + if (!book.canUpdate) return + if (chapterSize - durChapterIndex - 1 >= 3) return + if (System.currentTimeMillis() - book.lastCheckTime < 600000) return + book.lastCheckTime = System.currentTimeMillis() + val oldBook = book.copy() + WebBook.getChapterList(this, bookSource, book).onSuccess(IO) { cList -> + ensureActive() + if (cList.size > chapterSize) { + if (oldBook.bookUrl == book.bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(oldBook.bookUrl) + appDb.bookChapterDao.insert(*cList.toTypedArray()) + onChapterListUpdated(book, false) + nextTextChapter ?: loadContent(durChapterIndex + 1) + } + } + } + + fun pageAnim(): Int { + return book?.getPageAnim() ?: ReadBookConfig.pageAnim + } + + fun setCharset(charset: String) { + book?.let { + it.charset = charset + callBack?.loadChapterList(it) + } + saveRead() + } + + fun saveRead(pageChanged: Boolean = false) { + executor.execute { + kotlin.runCatching { + val book = book ?: return@execute + book.lastCheckCount = 0 + book.durChapterTime = System.currentTimeMillis() + val chapterChanged = book.durChapterIndex != durChapterIndex + book.durChapterIndex = durChapterIndex + book.durChapterPos = durChapterPos + if (!pageChanged || chapterChanged) { + appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let { + book.durChapterTitle = it.getDisplayTitle( + ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(), + book.getUseReplaceRule() + ) + } + } + appDb.bookDao.update(book) + }.onFailure { + AppLog.put("保存书籍阅读进度信息出错\n$it", it) + } + } + } + + /** + * 预下载 + */ + private fun preDownload() { + if (book?.isLocal == true) return + executor.execute { + if (AppConfig.preDownloadNum < 2) { + upToc() + return@execute + } + preDownloadTask?.cancel() + preDownloadTask = launch(IO) { + //预下载 + launch { + val maxChapterIndex = + min(durChapterIndex + AppConfig.preDownloadNum, chapterSize) + for (i in durChapterIndex.plus(2)..maxChapterIndex) { + if (downloadedChapters.contains(i)) continue + if ((downloadFailChapters[i] ?: 0) >= 3) continue + downloadIndex(i) + } + } + launch { + val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum) + for (i in durChapterIndex.minus(2) downTo minChapterIndex) { + if (downloadedChapters.contains(i)) continue + if ((downloadFailChapters[i] ?: 0) >= 3) continue + downloadIndex(i) + } + } + } + } + } + + fun cancelPreDownloadTask() { + if (contentLoadFinish) { + preDownloadTask?.cancel() + downloadScope.coroutineContext.cancelChildren() + } + } + + fun onChapterListUpdated(newBook: Book, loadContent: Boolean = true) { + if (newBook.isSameNameAuthor(book)) { + book = newBook + chapterSize = newBook.totalChapterNum + simulatedChapterSize = newBook.simulatedTotalChapterNum() + if (simulatedChapterSize > 0 && durChapterIndex > simulatedChapterSize - 1) { + durChapterIndex = simulatedChapterSize - 1 + } + callBack?.upMenuView() + if (callBack == null) { + clearTextChapter() + } else if (loadContent) { + loadContent(true) + } + } + } + + private fun clearExpiredChapterLoadingJob(clearAll: Boolean = false) { + val iterator = chapterLoadingJobs.iterator() + while (iterator.hasNext()) { + val (index, job) = iterator.next() + if (clearAll || index !in durChapterIndex - 1..durChapterIndex + 1) { + job.cancel() + iterator.remove() + } + } + } + + /** + * 注册回调 + */ + fun register(cb: CallBack) { + callBack?.notifyBookChanged() + callBack = cb + } + + /** + * 取消注册回调 + */ + fun unregister(cb: CallBack) { + if (callBack === cb) { + callBack = null + } + releaseAndCancel() + } + + private fun releaseAndCancel() { + msg = null + preDownloadTask?.cancel() + downloadScope.coroutineContext.cancelChildren() + coroutineContext.cancelChildren() + ImageProvider.clear() + clearExpiredChapterLoadingJob(true) + if (!CacheBookService.isRun) { + CacheBook.close() + } + } + + interface CallBack : LayoutProgressListener { + fun upMenuView() + + fun loadChapterList(book: Book) + + fun upContent( + relativePosition: Int = 0, + resetPageOffset: Boolean = true, + success: (() -> Unit)? = null + ) + + suspend fun upContentAwait( + relativePosition: Int = 0, + resetPageOffset: Boolean = true, + success: (() -> Unit)? = null + ) + + fun pageChanged() + + fun contentLoadFinish() + + fun upPageAnim(upRecorder: Boolean = false) + + fun notifyBookChanged() + + fun sureNewProgress(progress: BookProgress) + + fun cancelSelect() + } + +} diff --git a/app/src/main/java/io/legado/app/model/ReadManga.kt b/app/src/main/java/io/legado/app/model/ReadManga.kt new file mode 100755 index 000000000..3fca5f5b7 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/ReadManga.kt @@ -0,0 +1,640 @@ +package io.legado.app.model + +import io.legado.app.constant.AppLog +import io.legado.app.data.appDb +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookProgress +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.ReadRecord +import io.legado.app.help.AppWebDav +import io.legado.app.help.ConcurrentRateLimiter +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.ContentProcessor +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isSameNameAuthor +import io.legado.app.help.book.readSimulating +import io.legado.app.help.book.simulatedTotalChapterNum +import io.legado.app.help.book.update +import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.globalExecutor +import io.legado.app.model.webBook.WebBook +import io.legado.app.ui.book.manga.entities.BaseMangaPage +import io.legado.app.ui.book.manga.entities.MangaChapter +import io.legado.app.ui.book.manga.entities.MangaContent +import io.legado.app.ui.book.manga.entities.MangaPage +import io.legado.app.ui.book.manga.entities.ReaderLoading +import io.legado.app.utils.mapIndexed +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlin.math.min + +@Suppress("MemberVisibilityCanBePrivate") +object ReadManga : CoroutineScope by MainScope() { + var inBookshelf = false + var book: Book? = null + val executor = globalExecutor + var durChapterIndex = 0 //章节位置 + var chapterSize = 0//总章节 + var durChapterPos = 0 + var chapterChanged = false + var prevMangaChapter: MangaChapter? = null + var curMangaChapter: MangaChapter? = null + var nextMangaChapter: MangaChapter? = null + var bookSource: BookSource? = null + var readStartTime: Long = System.currentTimeMillis() + private val readRecord = ReadRecord() + private val loadingChapters = arrayListOf() + var simulatedChapterSize = 0 + var mCallback: Callback? = null + var preDownloadTask: Job? = null + val downloadedChapters = hashSetOf() + val downloadFailChapters = hashMapOf() + val downloadScope = CoroutineScope(SupervisorJob() + IO) + val preDownloadSemaphore = Semaphore(2) + var rateLimiter = ConcurrentRateLimiter(null) + val mangaContents get() = buildMangaContent() + val hasNextChapter get() = durChapterIndex < simulatedChapterSize - 1 + + fun resetData(book: Book) { + ReadManga.book = book + readRecord.bookName = book.name + readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0 + chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) + simulatedChapterSize = if (book.readSimulating()) { + book.simulatedTotalChapterNum() + } else { + chapterSize + } + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + clearMangaChapter() + upWebBook(book) + synchronized(this) { + loadingChapters.clear() + downloadedChapters.clear() + downloadFailChapters.clear() + } + } + + fun upData(book: Book) { + ReadManga.book = book + chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) + simulatedChapterSize = if (book.readSimulating()) { + book.simulatedTotalChapterNum() + } else { + chapterSize + } + + if (durChapterIndex != book.durChapterIndex) { + durChapterIndex = book.durChapterIndex + durChapterPos = book.durChapterPos + clearMangaChapter() + } + upWebBook(book) + synchronized(this) { + loadingChapters.clear() + downloadedChapters.clear() + downloadFailChapters.clear() + } + } + + fun upWebBook(book: Book) { + appDb.bookSourceDao.getBookSource(book.origin)?.let { + bookSource = it + rateLimiter = ConcurrentRateLimiter(it) + } ?: let { + bookSource = null + } + } + + fun clearMangaChapter() { + prevMangaChapter = null + curMangaChapter = null + nextMangaChapter = null + } + + //每次切换章节更新阅读记录 + fun upReadTime() { + executor.execute { + if (!AppConfig.enableReadRecord) { + return@execute + } + readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime + readStartTime = System.currentTimeMillis() + readRecord.lastRead = System.currentTimeMillis() + appDb.readRecordDao.insert(readRecord) + } + } + + @Synchronized + private fun addLoading(index: Int): Boolean { + if (loadingChapters.contains(index)) return false + loadingChapters.add(index) + return true + } + + @Synchronized + fun removeLoading(index: Int) { + loadingChapters.remove(index) + } + + fun loadContent() { + clearMangaChapter() + loadContent(durChapterIndex) + loadContent(durChapterIndex + 1) + loadContent(durChapterIndex - 1) + } + + fun loadOrUpContent() { + if (curMangaChapter == null) { + loadContent(durChapterIndex) + } else { + mCallback?.upContent() + } + if (nextMangaChapter == null) { + loadContent(durChapterIndex + 1) + } + if (prevMangaChapter == null) { + loadContent(durChapterIndex - 1) + } + } + + private fun loadContent(index: Int) { + Coroutine.async { + val book = book!! + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return@async + if (addLoading(index)) { + BookHelp.getContent(book, chapter)?.let { + contentLoadFinish(chapter, it) + } ?: run { + download(downloadScope, chapter) + } + } + }.onError { + AppLog.put("加载正文出错\n${it.localizedMessage}") + } + } + + /** + * 内容加载完成 + */ + suspend fun contentLoadFinish( + chapter: BookChapter, + content: String?, + errorMsg: String = "加载内容失败", + canceled: Boolean = false + ) { + removeLoading(chapter.index) + if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) { + return + } + when (val offset = chapter.index - durChapterIndex) { + 0 -> { + if (content == null) { + mCallback?.loadFail(errorMsg) + return + } + if (content.isEmpty() && !chapter.isVolume) { + mCallback?.loadFail("正文内容为空") + return + } + val mangaChapter = getManageChapter(chapter, content) + if (mangaChapter.imageCount == 0 && !chapter.isVolume) { + mCallback?.loadFail("正文没有图片") + return + } + curMangaChapter = mangaChapter + mCallback?.upContent() + } + + -1, 1 -> { + if (content == null || (!chapter.isVolume && content.isEmpty())) { + return + } + val mangaChapter = getManageChapter(chapter, content) + if (mangaChapter.imageCount == 0 && !chapter.isVolume) { + return + } + + when (offset) { + -1 -> prevMangaChapter = mangaChapter + 1 -> nextMangaChapter = mangaChapter + } + + mCallback?.upContent() + } + } + } + + private fun buildMangaContent(): MangaContent { + val items = arrayListOf() + var pos = 0 + var curFinish = false + var nextFinish = false + prevMangaChapter?.let { + pos += it.pages.size + items.addAll(it.pages) + } + curMangaChapter?.let { + curFinish = true + items.addAll(it.pages) + durChapterPos = if (it.imageCount > 0) { + durChapterPos.coerceIn(0, it.imageCount - 1) + } else { + 0 + } + pos += durChapterPos + if (!AppConfig.hideMangaTitle && it.imageCount > 0) { + pos++ + } + } + nextMangaChapter?.let { + nextFinish = true + items.addAll(it.pages) + } + return MangaContent(pos, items, curFinish, nextFinish) + } + + /** + * 加载下一章 + */ + fun moveToNextChapter(toFirst: Boolean = false): Boolean { + if (durChapterIndex < simulatedChapterSize - 1) { + if (toFirst) { + mCallback?.showLoading() + durChapterPos = 0 + } + durChapterIndex++ + prevMangaChapter = curMangaChapter + curMangaChapter = nextMangaChapter + nextMangaChapter = null + if (curMangaChapter == null) { + mCallback?.startLoad() + loadContent(durChapterIndex) + } else { + mCallback?.upContent() + } + loadContent(durChapterIndex + 1) + saveRead() + AppLog.putDebug("moveToNextChapter-curPageChanged()") + curPageChanged() + return true + } else { + AppLog.putDebug("跳转下一章失败,没有下一章") + return false + } + } + + fun moveToPrevChapter(toFirst: Boolean = false): Boolean { + if (durChapterIndex > 0) { + if (toFirst) { + mCallback?.showLoading() + durChapterPos = 0 + } + durChapterIndex-- + nextMangaChapter = curMangaChapter + curMangaChapter = prevMangaChapter + prevMangaChapter = null + if (curMangaChapter == null) { + loadContent(durChapterIndex) + } else { + mCallback?.upContent() + } + loadContent(durChapterIndex - 1) + saveRead() + return true + } + return false + } + + fun curPageChanged() { + upReadTime() + preDownload() + } + + fun saveRead(pageChanged: Boolean = false) { + executor.execute { + kotlin.runCatching { + val book = book ?: return@execute + book.lastCheckCount = 0 + book.durChapterTime = System.currentTimeMillis() + val chapterChanged = book.durChapterIndex != durChapterIndex + book.durChapterIndex = durChapterIndex + book.durChapterPos = durChapterPos + if (!pageChanged || chapterChanged) { + appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let { + book.durChapterTitle = it.getDisplayTitle( + ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(), + book.getUseReplaceRule() + ) + } + } + appDb.bookDao.update(book) + }.onFailure { + AppLog.put("保存漫画阅读进度信息出错\n$it", it) + } + } + } + + private fun downloadNetworkContent( + bookSource: BookSource, + scope: CoroutineScope, + chapter: BookChapter, + book: Book, + semaphore: Semaphore?, + success: suspend (String) -> Unit = {}, + error: suspend () -> Unit = {}, + cancel: suspend () -> Unit = {}, + ) { + WebBook.getContent( + scope, + bookSource, + book, + chapter, + start = CoroutineStart.LAZY, + executeContext = IO, + semaphore = semaphore + ).onSuccess { content -> + success.invoke(content) + }.onError { + error.invoke() + }.onCancel { + cancel.invoke() + }.start() + } + + private fun preDownload() { + if (book?.isLocal == true) return + executor.execute { + if (AppConfig.preDownloadNum < 2) { + upToc() + return@execute + } + preDownloadTask?.cancel() + preDownloadTask = launch(IO) { + //预下载 + launch { + val maxChapterIndex = + min(durChapterIndex + AppConfig.preDownloadNum, chapterSize) + for (i in durChapterIndex.plus(2)..maxChapterIndex) { + if (downloadedChapters.contains(i)) continue + if ((downloadFailChapters[i] ?: 0) >= 3) continue + downloadIndex(i) + } + } + launch { + val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum) + for (i in durChapterIndex.minus(2) downTo minChapterIndex) { + if (downloadedChapters.contains(i)) continue + if ((downloadFailChapters[i] ?: 0) >= 3) continue + downloadIndex(i) + } + } + } + } + } + + fun cancelPreDownloadTask() { + if (curMangaChapter != null && nextMangaChapter != null) { + preDownloadTask?.cancel() + downloadScope.coroutineContext.cancelChildren() + } + } + + private suspend fun downloadIndex(index: Int) { + if (index < 0) return + if (index > chapterSize - 1) { + upToc() + return + } + val book = book ?: return + val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, index) ?: return + if (BookHelp.hasContent(book, chapter)) { + downloadedChapters.add(chapter.index) + } else { + delay(1000) + if (addLoading(index)) { + download(downloadScope, chapter, preDownloadSemaphore) + } + } + } + + /** + * 获取正文 + */ + private suspend fun download( + scope: CoroutineScope, + chapter: BookChapter, + semaphore: Semaphore? = null, + ) { + val book = book ?: return removeLoading(chapter.index) + val bookSource = bookSource + if (bookSource != null) { + downloadNetworkContent(bookSource, scope, chapter, book, semaphore, success = { + downloadedChapters.add(chapter.index) + downloadFailChapters.remove(chapter.index) + contentLoadFinish(chapter, it) + }, error = { + downloadFailChapters[chapter.index] = + (downloadFailChapters[chapter.index] ?: 0) + 1 + contentLoadFinish(chapter, null) + }, cancel = { + contentLoadFinish(chapter, null, canceled = true) + }) + } else { + contentLoadFinish(chapter, null, "加载内容失败 没有书源") + } + } + + @Synchronized + fun upToc() { + val bookSource = bookSource ?: return + val book = book ?: return + if (!book.canUpdate) return + if (chapterSize - durChapterIndex - 1 >= 3) return + if (System.currentTimeMillis() - book.lastCheckTime < 600000) return + book.lastCheckTime = System.currentTimeMillis() + val oldBook = book.copy() + WebBook.getChapterList(this, bookSource, book).onSuccess(IO) { cList -> + ensureActive() + if (cList.size > chapterSize) { + if (oldBook.bookUrl == book.bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(oldBook.bookUrl) + appDb.bookChapterDao.insert(*cList.toTypedArray()) + onChapterListUpdated(book, false) + nextMangaChapter ?: loadContent(durChapterIndex + 1) + } + } + } + + fun uploadProgress(successAction: (() -> Unit)? = null) { + book?.let { + launch(IO) { + AppWebDav.uploadBookProgress(it) { + successAction?.invoke() + } + ensureActive() + it.update() + } + } + } + + /** + * 同步阅读进度 + * 如果当前进度快于服务器进度或者没有进度进行上传,如果慢与服务器进度则执行传入动作 + */ + fun syncProgress( + newProgressAction: ((progress: BookProgress) -> Unit)? = null, + uploadSuccessAction: (() -> Unit)? = null, + syncSuccessAction: (() -> Unit)? = null, + ) { + if (!AppConfig.syncBookProgress) return + val book = book ?: return + Coroutine.async { + AppWebDav.getBookProgress(book) + }.onError { + AppLog.put("拉取阅读进度失败", it) + }.onSuccess { progress -> + if (progress == null || progress.durChapterIndex < book.durChapterIndex || + (progress.durChapterIndex == book.durChapterIndex + && progress.durChapterPos < book.durChapterPos) + ) { + // 服务器没有进度或者进度比服务器快,上传现有进度 + Coroutine.async { + AppWebDav.uploadBookProgress(BookProgress(book), uploadSuccessAction) + book.update() + } + } else if (progress.durChapterIndex > book.durChapterIndex || + progress.durChapterPos > book.durChapterPos + ) { + // 进度比服务器慢,执行传入动作 + newProgressAction?.invoke(progress) + } else { + syncSuccessAction?.invoke() + } + } + } + + fun setProgress(progress: BookProgress) { + if (progress.durChapterIndex < chapterSize && + (durChapterIndex != progress.durChapterIndex + || durChapterPos != progress.durChapterPos) + ) { + mCallback?.showLoading() + if (progress.durChapterIndex == durChapterIndex) { + durChapterPos = progress.durChapterPos + mCallback?.upContent() + } else { + durChapterIndex = progress.durChapterIndex + durChapterPos = progress.durChapterPos + loadContent() + } + saveRead() + } + } + + fun showLoading() { + mCallback?.showLoading() + } + + fun loadFail(msg: String, retry: Boolean = true) { + mCallback?.loadFail(msg, retry) + } + + fun onChapterListUpdated(newBook: Book, loadContent: Boolean = true) { + if (newBook.isSameNameAuthor(book)) { + book = newBook + chapterSize = newBook.totalChapterNum + simulatedChapterSize = newBook.simulatedTotalChapterNum() + if (simulatedChapterSize > 0 && durChapterIndex > simulatedChapterSize - 1) { + durChapterIndex = simulatedChapterSize - 1 + } + if (mCallback == null) { + clearMangaChapter() + } else if (loadContent) { + loadContent() + } + } + } + + /** + * 注册回调 + */ + fun register(cb: Callback) { + mCallback = cb + } + + /** + * 取消注册回调 + */ + fun unregister(cb: Callback) { + if (mCallback === cb) { + mCallback = null + } + preDownloadTask?.cancel() + preDownloadTask = null + downloadScope.coroutineContext.cancelChildren() + coroutineContext.cancelChildren() + } + + private suspend fun getManageChapter(chapter: BookChapter, content: String): MangaChapter { + val list = BookHelp.flowImages(chapter, content) + .distinctUntilChanged().mapIndexed { index, src -> + MangaPage( + chapterIndex = chapter.index, + chapterSize = chapterSize, + mImageUrl = src, + index = index, + mChapterName = chapter.title + ) + }.toList() + + val imageCount = list.size + + list.forEach { + it.imageCount = imageCount + } + + if (AppConfig.hideMangaTitle && imageCount > 0) { + return MangaChapter(chapter, list, imageCount) + } + + val pages = mutableListOf() + + if (imageCount == 0 && chapter.isVolume) { + pages.add(ReaderLoading(chapter.index, -1, chapter.title, true)) + } else { + pages.add(ReaderLoading(chapter.index, -1, "阅读 ${chapter.title}")) + pages.addAll(list) + pages.add(ReaderLoading(chapter.index, imageCount, "已读完 ${chapter.title}")) + } + + return MangaChapter(chapter, pages, imageCount) + } + + interface Callback { + fun upContent() + fun loadFail(msg: String, retry: Boolean = true) + fun sureNewProgress(progress: BookProgress) + fun showLoading() + fun startLoad() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/SharedJsScope.kt b/app/src/main/java/io/legado/app/model/SharedJsScope.kt new file mode 100755 index 000000000..b2bfe23b1 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/SharedJsScope.kt @@ -0,0 +1,87 @@ +package io.legado.app.model + +import androidx.collection.LruCache +import com.google.gson.reflect.TypeToken +import com.script.ScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.newCallStrResponse +import io.legado.app.help.http.okHttpClient +import io.legado.app.utils.ACache +import io.legado.app.utils.GSON +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.isJsonObject +import kotlinx.coroutines.runBlocking +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.ScriptableObject +import splitties.init.appCtx +import java.io.File +import java.lang.ref.WeakReference +import kotlin.coroutines.CoroutineContext + +object SharedJsScope { + + private val cacheFolder = File(appCtx.cacheDir, "shareJs") + private val aCache = ACache.get(cacheFolder) + + private val scopeMap = LruCache>(16) + + fun getScope(jsLib: String?, coroutineContext: CoroutineContext?): Scriptable? { + if (jsLib.isNullOrBlank()) { + return null + } + val key = MD5Utils.md5Encode(jsLib) + var scope = scopeMap[key]?.get() + if (scope == null) { + scope = RhinoScriptEngine.run { + getRuntimeScope(ScriptBindings()) + } + if (jsLib.isJsonObject()) { + val jsMap: Map = GSON.fromJson( + jsLib, + TypeToken.getParameterized( + Map::class.java, + String::class.java, + String::class.java + ).type + ) + jsMap.values.forEach { value -> + if (value.isAbsUrl()) { + val fileName = MD5Utils.md5Encode(value) + var js = aCache.getAsString(fileName) + if (js == null) { + js = runBlocking { + okHttpClient.newCallStrResponse { + url(value) + }.body + } + if (js != null) { + aCache.put(fileName, js) + } else { + throw NoStackTraceException("下载jsLib-${value}失败") + } + } + RhinoScriptEngine.eval(js, scope, coroutineContext) + } + } + } else { + RhinoScriptEngine.eval(jsLib, scope, coroutineContext) + } + if (scope is ScriptableObject) { + scope.sealObject() + } + scopeMap.put(key, WeakReference(scope)) + } + return scope + } + + fun remove(jsLib: String?) { + if (jsLib.isNullOrBlank()) { + return + } + val key = MD5Utils.md5Encode(jsLib) + scopeMap.remove(key) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt new file mode 100755 index 000000000..71d9006ce --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt @@ -0,0 +1,172 @@ +package io.legado.app.model.analyzeRule + +import androidx.annotation.Keep +import com.jayway.jsonpath.JsonPath +import com.jayway.jsonpath.ReadContext +import io.legado.app.utils.printOnDebug + + +@Suppress("RegExpRedundantEscape") +@Keep +class AnalyzeByJSonPath(json: Any) { + + companion object { + + fun parse(json: Any): ReadContext { + return when (json) { + is ReadContext -> json + is String -> JsonPath.parse(json) //JsonPath.parse(json) + else -> JsonPath.parse(json) //JsonPath.parse(json) + } + } + } + + private var ctx: ReadContext = parse(json) + + /** + * 改进解析方法 + * 解决阅读”&&“、”||“与jsonPath支持的”&&“、”||“之间的冲突 + * 解决{$.rule}形式规则可能匹配错误的问题,旧规则用正则解析内容含‘}’的json文本时,用规则中的字段去匹配这种内容会匹配错误.现改用平衡嵌套方法解决这个问题 + * */ + fun getString(rule: String): String? { + if (rule.isEmpty()) return null + var result: String + val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 + val rules = ruleAnalyzes.splitRule("&&", "||") + + if (rules.size == 1) { + + ruleAnalyzes.reSetPos() //将pos重置为0,复用解析器 + + result = ruleAnalyzes.innerRule("{$.") { getString(it) } //替换所有{$.rule...} + + if (result.isEmpty()) { //st为空,表明无成功替换的内嵌规则 + try { + val ob = ctx.read(rule) + result = if (ob is List<*>) { + ob.joinToString("\n") + } else { + ob.toString() + } + } catch (e: Exception) { + e.printOnDebug() + } + } + return result + } else { + val textList = arrayListOf() + for (rl in rules) { + val temp = getString(rl) + if (!temp.isNullOrEmpty()) { + textList.add(temp) + if (ruleAnalyzes.elementsType == "||") { + break + } + } + } + return textList.joinToString("\n") + } + } + + internal fun getStringList(rule: String): List { + val result = ArrayList() + if (rule.isEmpty()) return result + val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + + if (rules.size == 1) { + ruleAnalyzes.reSetPos() //将pos重置为0,复用解析器 + val st = ruleAnalyzes.innerRule("{$.") { getString(it) } //替换所有{$.rule...} + if (st.isEmpty()) { //st为空,表明无成功替换的内嵌规则 + try { + val obj = ctx.read(rule) + if (obj is List<*>) { + for (o in obj) result.add(o.toString()) + } else { + result.add(obj.toString()) + } + } catch (e: Exception) { + e.printOnDebug() + } + } else { + result.add(st) + } + return result + } else { + val results = ArrayList>() + for (rl in rules) { + val temp = getStringList(rl) + if (temp.isNotEmpty()) { + results.add(temp) + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { + break + } + } + } + if (results.size > 0) { + if ("%%" == ruleAnalyzes.elementsType) { + for (i in results[0].indices) { + for (temp in results) { + if (i < temp.size) { + result.add(temp[i]) + } + } + } + } else { + for (temp in results) { + result.addAll(temp) + } + } + } + return result + } + } + + internal fun getObject(rule: String): Any { + return ctx.read(rule) + } + + internal fun getList(rule: String): ArrayList? { + val result = ArrayList() + if (rule.isEmpty()) return result + val ruleAnalyzes = RuleAnalyzer(rule, true) //设置平衡组为代码平衡 + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + if (rules.size == 1) { + ctx.let { + try { + return it.read>(rules[0]) + } catch (e: Exception) { + e.printOnDebug() + } + } + } else { + val results = ArrayList>() + for (rl in rules) { + val temp = getList(rl) + if (!temp.isNullOrEmpty()) { + results.add(temp) + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { + break + } + } + } + if (results.size > 0) { + if ("%%" == ruleAnalyzes.elementsType) { + for (i in 0 until results[0].size) { + for (temp in results) { + if (i < temp.size) { + temp[i]?.let { result.add(it) } + } + } + } + } else { + for (temp in results) { + result.addAll(temp) + } + } + } + } + return result + } + +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt new file mode 100755 index 000000000..e6103d1e6 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt @@ -0,0 +1,524 @@ +package io.legado.app.model.analyzeRule + +import androidx.annotation.Keep +import org.jsoup.Jsoup +import org.jsoup.nodes.Element +import org.jsoup.parser.Parser +import org.jsoup.select.Collector +import org.jsoup.select.Elements +import org.jsoup.select.Evaluator +import org.seimicrawler.xpath.JXNode + +/** + * Created by GKF on 2018/1/25. + * 书源规则解析 + */ +@Keep +class AnalyzeByJSoup(doc: Any) { + + companion object { + private val nullSet = setOf(null) + } + + private var element: Element = parse(doc) + + private fun parse(doc: Any): Element { + if (doc is Element) { + return doc + } + if (doc is JXNode) { + return if (doc.isElement) doc.asElement() else Jsoup.parse(doc.toString()) + } + kotlin.runCatching { + if (doc.toString().startsWith(" { + + val textS = ArrayList() + + if (ruleStr.isEmpty()) return textS + + //拆分规则 + val sourceRule = SourceRule(ruleStr) + + if (sourceRule.elementsRule.isEmpty()) { + + textS.add(element.data() ?: "") + + } else { + + val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) + val ruleStrS = ruleAnalyzes.splitRule("&&", "||", "%%") + + val results = ArrayList>() + for (ruleStrX in ruleStrS) { + + val temp: ArrayList? = + if (sourceRule.isCss) { + val lastIndex = ruleStrX.lastIndexOf('@') + getResultLast( + element.select(ruleStrX.substring(0, lastIndex)), + ruleStrX.substring(lastIndex + 1) + ) + } else { + getResultList(ruleStrX) + } + + if (!temp.isNullOrEmpty()) { + results.add(temp) + if (ruleAnalyzes.elementsType == "||") break + } + } + if (results.size > 0) { + if ("%%" == ruleAnalyzes.elementsType) { + for (i in results[0].indices) { + for (temp in results) { + if (i < temp.size) { + textS.add(temp[i]) + } + } + } + } else { + for (temp in results) { + textS.addAll(temp) + } + } + } + } + return textS + } + + /** + * 获取Elements + */ + private fun getElements(temp: Element?, rule: String): Elements { + + if (temp == null || rule.isEmpty()) return Elements() + + val elements = Elements() + + val sourceRule = SourceRule(rule) + val ruleAnalyzes = RuleAnalyzer(sourceRule.elementsRule) + val ruleStrS = ruleAnalyzes.splitRule("&&", "||", "%%") + + val elementsList = ArrayList() + if (sourceRule.isCss) { + for (ruleStr in ruleStrS) { + val tempS = temp.select(ruleStr) + elementsList.add(tempS) + if (tempS.size > 0 && ruleAnalyzes.elementsType == "||") { + break + } + } + } else { + for (ruleStr in ruleStrS) { + + val rsRule = RuleAnalyzer(ruleStr) + + rsRule.trim() // 修剪当前规则之前的"@"或者空白符 + + val rs = rsRule.splitRule("@") + + val el = if (rs.size > 1) { + val el = Elements() + el.add(temp) + for (rl in rs) { + val es = Elements() + for (et in el) { + es.addAll(getElements(et, rl)) + } + el.clear() + el.addAll(es) + } + el + } else ElementsSingle().getElementsSingle(temp, ruleStr) + + elementsList.add(el) + if (el.size > 0 && ruleAnalyzes.elementsType == "||") { + break + } + } + } + if (elementsList.size > 0) { + if ("%%" == ruleAnalyzes.elementsType) { + for (i in 0 until elementsList[0].size) { + for (es in elementsList) { + if (i < es.size) { + elements.add(es[i]) + } + } + } + } else { + for (es in elementsList) { + elements.addAll(es) + } + } + } + return elements + } + + /** + * 获取内容列表 + */ + private fun getResultList(ruleStr: String): ArrayList? { + + if (ruleStr.isEmpty()) return null + + var elements = Elements() + + elements.add(element) + + val rule = RuleAnalyzer(ruleStr) //创建解析 + + rule.trim() //修建前置赘余符号 + + val rules = rule.splitRule("@") // 切割成列表 + + val last = rules.size - 1 + for (i in 0 until last) { + val es = Elements() + for (elt in elements) { + es.addAll(ElementsSingle().getElementsSingle(elt, rules[i])) + } + elements.clear() + elements = es + } + return if (elements.isEmpty()) null else getResultLast(elements, rules[last]) + } + + /** + * 根据最后一个规则获取内容 + */ + private fun getResultLast(elements: Elements, lastRule: String): ArrayList { + val textS = ArrayList() + when (lastRule) { + "text" -> for (element in elements) { + val text = element.text() + if (text.isNotEmpty()) { + textS.add(text) + } + } + + "textNodes" -> for (element in elements) { + val tn = arrayListOf() + val contentEs = element.textNodes() + for (item in contentEs) { + val text = item.text().trim { it <= ' ' } + if (text.isNotEmpty()) { + tn.add(text) + } + } + if (tn.isNotEmpty()) { + textS.add(tn.joinToString("\n")) + } + } + + "ownText" -> for (element in elements) { + val text = element.ownText() + if (text.isNotEmpty()) { + textS.add(text) + } + } + + "html" -> { + elements.select("script").remove() + elements.select("style").remove() + val html = elements.outerHtml() + if (html.isNotEmpty()) { + textS.add(html) + } + } + + "all" -> textS.add(elements.outerHtml()) + else -> for (element in elements) { + + val url = element.attr(lastRule) + + if (url.isBlank() || textS.contains(url)) continue + + textS.add(url) + } + } + return textS + } + + /** + * 1.支持阅读原有写法,':'分隔索引,!或.表示筛选方式,索引可为负数 + * 例如 tag.div.-1:10:2 或 tag.div!0:3 + * + * 2. 支持与jsonPath类似的[]索引写法 + * 格式形如 [it,it,。。。] 或 [!it,it,。。。] 其中[!开头表示筛选方式为排除,it为单个索引或区间。 + * 区间格式为 start:end 或 start:end:step,其中start为0可省略,end为-1可省略。 + * 索引,区间两端及间隔都支持负数 + * 例如 tag.div[-1, 3:-2:-10, 2] + * 特殊用法 tag.div[-1:0] 可在任意地方让列表反向 + * */ + @Suppress("UNCHECKED_CAST") + data class ElementsSingle( + var split: Char = '.', + var beforeRule: String = "", + val indexDefault: MutableList = mutableListOf(), + val indexes: MutableList = mutableListOf() + ) { + /** + * 获取Elements按照一个规则 + */ + fun getElementsSingle(temp: Element, rule: String): Elements { + + findIndexSet(rule) //执行索引列表处理器 + + /** + * 获取所有元素 + * */ + var elements = + if (beforeRule.isEmpty()) temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 + else { + val rules = beforeRule.split(".") + when (rules[0]) { + "children" -> temp.children() //允许索引直接作为根元素,此时前置规则为空,效果与children相同 + "class" -> temp.getElementsByClass(rules[1]) + "tag" -> temp.getElementsByTag(rules[1]) + "id" -> Collector.collect(Evaluator.Id(rules[1]), temp) + "text" -> temp.getElementsContainingOwnText(rules[1]) + else -> temp.select(beforeRule) + } + } + + val len = elements.size + val lastIndexes = (indexDefault.size - 1).takeIf { it != -1 } ?: (indexes.size - 1) + val indexSet = mutableSetOf() + + /** + * 获取无重且不越界的索引集合 + * */ + if (indexes.isEmpty()) for (ix in lastIndexes downTo 0) { //indexes为空,表明是非[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 + + val it = indexDefault[ix] + if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 + else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 + + } else for (ix in lastIndexes downTo 0) { //indexes不空,表明是[]式索引,集合是逆向遍历插入的,所以这里也逆向遍历,好还原顺序 + + if (indexes[ix] is Triple<*, *, *>) { //区间 + val (startX, endX, stepX) = indexes[ix] as Triple //还原储存时的类型 + + var start = startX ?: 0 // 左端省略表示0 + if (start < 0) start += len // 将负索引转正 + + var end = endX ?: (len - 1) // 右端省略表示 len - 1 + if (end < 0) end += len // 将负索引转正 + + if ((start < 0 && end < 0) || (start >= len && end >= len)) { + // start 和 end 同侧左右端越界,无效索引 + continue + } + + if (start >= len) start = len - 1 // 右端越界,设置为最大索引 + else if (start < 0) start = 0 // 左端越界,设置为最小索引 + + if (end >= len) end = len - 1 // 右端越界,设置为最大索引 + else if (end < 0) end = 0 // 左端越界,设置为最小索引 + + if (start == end || stepX >= len) { //两端相同,区间里只有一个数。或间隔过大,区间实际上仅有首位 + + indexSet.add(start) + continue + + } + + val step = + if (stepX > 0) stepX else if (-stepX < len) stepX + len else 1 //最小正数间隔为1 + + //将区间展开到集合中,允许列表反向。 + indexSet.addAll(if (end > start) start..end step step else start downTo end step step) + + } else {//单个索引 + + val it = indexes[ix] as Int //还原储存时的类型 + + if (it in 0 until len) indexSet.add(it) //将正数不越界的索引添加到集合 + else if (it < 0 && len >= -it) indexSet.add(it + len) //将负数不越界的索引添加到集合 + + } + + } + + /** + * 根据索引集合筛选元素 + * */ + if (split == '!') { //排除 + + for (pcInt in indexSet) elements[pcInt] = null + + elements.removeAll(nullSet) //测试过,这样就行 + + } else if (split == '.') { //选择 + + val es = Elements() + + for (pcInt in indexSet) es.add(elements[pcInt]) + + elements = es + + } + + return elements //返回筛选结果 + + } + + private fun findIndexSet(rule: String) { + + val rus = rule.trim { it <= ' ' } + + var len = rus.length + var curInt: Int? //当前数字 + var curMinus = false //当前数字是否为负 + val curList = mutableListOf() //当前数字区间 + var l = "" //暂存数字字符串 + + val head = rus.last() == ']' //是否为常规索引写法 + + if (head) { //常规索引写法[index...] + + len-- //跳过尾部']' + + while (len-- >= 0) { //逆向遍历,可以无前置规则 + + var rl = rus[len] + if (rl == ' ') continue //跳过空格 + + if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出 + else if (rl == '-') curMinus = true + else { + + curInt = + if (l.isEmpty()) null else if (curMinus) -l.toInt() else l.toInt() //当前数字 + + when (rl) { + + ':' -> curList.add(curInt) //区间右端或区间间隔 + + else -> { + + //为保证查找顺序,区间和单个索引都添加到同一集合 + if (curList.isEmpty()) { + + if (curInt == null) break //是jsoup选择器而非索引列表,跳出 + + indexes.add(curInt) + } else { + + //列表最后压入的是区间右端,若列表有两位则最先压入的是间隔 + indexes.add( + Triple( + curInt, + curList.last(), + if (curList.size == 2) curList.first() else 1 + ) + ) + + curList.clear() //重置临时列表,避免影响到下个区间的处理 + + } + + if (rl == '!') { + split = '!' + do { + rl = rus[--len] + } while (len > 0 && rl == ' ')//跳过所有空格 + } + + if (rl == '[') { + beforeRule = rus.substring(0, len) //遇到索引边界,返回结果 + return + } + + if (rl != ',') break //非索引结构,跳出 + + } + } + + l = "" //清空 + curMinus = false //重置 + } + } + } else while (len-- >= 0) { //阅读原本写法,逆向遍历,可以无前置规则 + + val rl = rus[len] + if (rl == ' ') continue //跳过空格 + + if (rl in '0'..'9') l = rl + l //将数值累接入临时字串中,遇到分界符才取出 + else if (rl == '-') curMinus = true + else { + + if (rl == '!' || rl == '.' || rl == ':') { //分隔符或起始符 + + indexDefault.add(if (curMinus) -l.toInt() else l.toInt()) // 当前数字追加到列表 + + if (rl != ':') { //rl == '!' || rl == '.' + split = rl + beforeRule = rus.substring(0, len) + return + } + + } else break //非索引结构,跳出循环 + + l = "" //清空 + curMinus = false //重置 + } + } + + split = ' ' + beforeRule = rus + } + } + + + internal inner class SourceRule(ruleStr: String) { + var isCss = false + var elementsRule: String = if (ruleStr.startsWith("@CSS:", true)) { + isCss = true + ruleStr.substring(5).trim { it <= ' ' } + } else { + ruleStr + } + } + +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt new file mode 100755 index 000000000..7e41eba6a --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt @@ -0,0 +1,60 @@ +package io.legado.app.model.analyzeRule + +import androidx.annotation.Keep +import java.util.regex.Pattern + +@Keep +object AnalyzeByRegex { + + fun getElement(res: String, regs: Array, index: Int = 0): List? { + var vIndex = index + val resM = Pattern.compile(regs[vIndex]).matcher(res) + if (!resM.find()) { + return null + } + // 判断索引的规则是最后一个规则 + return if (vIndex + 1 == regs.size) { + // 新建容器 + val info = arrayListOf() + for (groupIndex in 0..resM.groupCount()) { + info.add(resM.group(groupIndex)!!) + } + info + } else { + val result = StringBuilder() + do { + result.append(resM.group()) + } while (resM.find()) + getElement(result.toString(), regs, ++vIndex) + } + } + + fun getElements(res: String, regs: Array, index: Int = 0): List> { + var vIndex = index + val resM = Pattern.compile(regs[vIndex]).matcher(res) + if (!resM.find()) { + return arrayListOf() + } + // 判断索引的规则是最后一个规则 + if (vIndex + 1 == regs.size) { + // 创建书息缓存数组 + val books = ArrayList>() + // 提取列表 + do { + // 新建容器 + val info = arrayListOf() + for (groupIndex in 0..resM.groupCount()) { + info.add(resM.group(groupIndex) ?: "") + } + books.add(info) + } while (resM.find()) + return books + } else { + val result = StringBuilder() + do { + result.append(resM.group()) + } while (resM.find()) + return getElements(result.toString(), regs, ++vIndex) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt new file mode 100755 index 000000000..b0d52cf3a --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt @@ -0,0 +1,155 @@ +package io.legado.app.model.analyzeRule + +import android.text.TextUtils +import androidx.annotation.Keep +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import org.jsoup.parser.Parser +import org.jsoup.select.Elements +import org.seimicrawler.xpath.JXDocument +import org.seimicrawler.xpath.JXNode + +@Keep +class AnalyzeByXPath(doc: Any) { + private var jxNode: Any = parse(doc) + + private fun parse(doc: Any): Any { + return when (doc) { + is JXNode -> if (doc.isElement) doc else strToJXDocument(doc.toString()) + is Document -> JXDocument.create(doc) + is Element -> JXDocument.create(Elements(doc)) + is Elements -> JXDocument.create(doc) + else -> strToJXDocument(doc.toString()) + } + } + + private fun strToJXDocument(html: String): JXDocument { + var html1 = html + if (html1.endsWith("")) { + html1 = "${html1}" + } + if (html1.endsWith("") || html1.endsWith("")) { + html1 = "${html1}
    " + } + kotlin.runCatching { + if (html1.trim().startsWith("? { + val node = jxNode + return if (node is JXNode) { + node.sel(xPath) + } else { + (node as JXDocument).selN(xPath) + } + } + + internal fun getElements(xPath: String): List? { + + if (xPath.isEmpty()) return null + + val jxNodes = ArrayList() + val ruleAnalyzes = RuleAnalyzer(xPath) + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + + if (rules.size == 1) { + return getResult(rules[0]) + } else { + val results = ArrayList>() + for (rl in rules) { + val temp = getElements(rl) + if (!temp.isNullOrEmpty()) { + results.add(temp) + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { + break + } + } + } + if (results.size > 0) { + if ("%%" == ruleAnalyzes.elementsType) { + for (i in results[0].indices) { + for (temp in results) { + if (i < temp.size) { + jxNodes.add(temp[i]) + } + } + } + } else { + for (temp in results) { + jxNodes.addAll(temp) + } + } + } + } + return jxNodes + } + + internal fun getStringList(xPath: String): List { + + val result = ArrayList() + val ruleAnalyzes = RuleAnalyzer(xPath) + val rules = ruleAnalyzes.splitRule("&&", "||", "%%") + + if (rules.size == 1) { + getResult(xPath)?.map { + result.add(it.asString()) + } + return result + } else { + val results = ArrayList>() + for (rl in rules) { + val temp = getStringList(rl) + if (temp.isNotEmpty()) { + results.add(temp) + if (temp.isNotEmpty() && ruleAnalyzes.elementsType == "||") { + break + } + } + } + if (results.size > 0) { + if ("%%" == ruleAnalyzes.elementsType) { + for (i in results[0].indices) { + for (temp in results) { + if (i < temp.size) { + result.add(temp[i]) + } + } + } + } else { + for (temp in results) { + result.addAll(temp) + } + } + } + } + return result + } + + fun getString(rule: String): String? { + val ruleAnalyzes = RuleAnalyzer(rule) + val rules = ruleAnalyzes.splitRule("&&", "||") + if (rules.size == 1) { + getResult(rule)?.let { + return TextUtils.join("\n", it) + } + return null + } else { + val textList = arrayListOf() + for (rl in rules) { + val temp = getString(rl) + if (!temp.isNullOrEmpty()) { + textList.add(temp) + if (ruleAnalyzes.elementsType == "||") { + break + } + } + } + return textList.joinToString("\n") + } + } +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt new file mode 100755 index 000000000..2e44cc0c9 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -0,0 +1,907 @@ +package io.legado.app.model.analyzeRule + +import android.text.TextUtils +import androidx.annotation.Keep +import com.script.CompiledScript +import com.script.buildScriptBindings +import com.script.rhino.RhinoScriptEngine +import io.legado.app.constant.AppPattern.JS_PATTERN +import io.legado.app.data.entities.BaseBook +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.RssArticle +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.CacheManager +import io.legado.app.help.JsExtensions +import io.legado.app.help.http.CookieStore +import io.legado.app.help.source.getShareScope +import io.legado.app.model.Debug +import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.GSON +import io.legado.app.utils.GSONStrict +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.getOrPutLimit +import io.legado.app.utils.isDataUrl +import io.legado.app.utils.isJson +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.stackTraceStr +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.apache.commons.text.StringEscapeUtils +import org.jsoup.nodes.Node +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.lang.ref.WeakReference +import java.net.URL +import java.util.Locale +import java.util.regex.Pattern +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * 解析规则获取结果 + */ +@Keep +@Suppress("unused", "RegExpRedundantEscape", "MemberVisibilityCanBePrivate") +class AnalyzeRule( + private var ruleData: RuleDataInterface? = null, + private val source: BaseSource? = null, + private val preUpdateJs: Boolean = false +) : JsExtensions { + + private val book get() = ruleData as? BaseBook + private val rssArticle get() = ruleData as? RssArticle + + private var chapter: BookChapter? = null + private var nextChapterUrl: String? = null + private var content: Any? = null + private var baseUrl: String? = null + private var redirectUrl: URL? = null + private var isJSON: Boolean = false + private var isRegex: Boolean = false + + private var analyzeByXPath: AnalyzeByXPath? = null + private var analyzeByJSoup: AnalyzeByJSoup? = null + private var analyzeByJSonPath: AnalyzeByJSonPath? = null + + private val stringRuleCache = hashMapOf>() + private val regexCache = hashMapOf() + private val scriptCache = hashMapOf() + private var topScopeRef: WeakReference? = null + private var evalJSCallCount = 0 + + private var coroutineContext: CoroutineContext = EmptyCoroutineContext + + private var loggedNonStandardJSON = false + + @JvmOverloads + fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule { + if (content == null) throw AssertionError("内容不可空(Content cannot be null)") + this.content = content + isJSON = when (content) { + is Node -> false + else -> content.toString().isJson() + } + setBaseUrl(baseUrl) + analyzeByXPath = null + analyzeByJSoup = null + analyzeByJSonPath = null + return this + } + + fun setBaseUrl(baseUrl: String?): AnalyzeRule { + baseUrl?.let { + this.baseUrl = baseUrl + } + return this + } + + fun setRedirectUrl(url: String): URL? { + if (url.isDataUrl()) { + return redirectUrl + } + try { + redirectUrl = URL(url) + } catch (e: Exception) { + log("URL($url) error\n${e.localizedMessage}") + } + return redirectUrl + } + + /** + * 获取XPath解析类 + */ + private fun getAnalyzeByXPath(o: Any): AnalyzeByXPath { + return if (o != content) { + AnalyzeByXPath(o) + } else { + if (analyzeByXPath == null) { + analyzeByXPath = AnalyzeByXPath(content!!) + } + analyzeByXPath!! + } + } + + /** + * 获取JSOUP解析类 + */ + private fun getAnalyzeByJSoup(o: Any): AnalyzeByJSoup { + return if (o != content) { + AnalyzeByJSoup(o) + } else { + if (analyzeByJSoup == null) { + analyzeByJSoup = AnalyzeByJSoup(content!!) + } + analyzeByJSoup!! + } + } + + /** + * 获取JSON解析类 + */ + private fun getAnalyzeByJSonPath(o: Any): AnalyzeByJSonPath { + return if (o != content) { + AnalyzeByJSonPath(o) + } else { + if (analyzeByJSonPath == null) { + analyzeByJSonPath = AnalyzeByJSonPath(content!!) + } + analyzeByJSonPath!! + } + } + + /** + * 获取文本列表 + */ + @JvmOverloads + fun getStringList(rule: String?, mContent: Any? = null, isUrl: Boolean = false): List? { + if (rule.isNullOrEmpty()) return null + val ruleList = splitSourceRuleCacheString(rule) + return getStringList(ruleList, mContent, isUrl) + } + + @JvmOverloads + fun getStringList( + ruleList: List, + mContent: Any? = null, + isUrl: Boolean = false + ): List? { + var result: Any? = null + val content = mContent ?: this.content + if (content != null && ruleList.isNotEmpty()) { + result = content + if (result is NativeObject) { + val sourceRule = ruleList.first() + putRule(sourceRule.putMap) + sourceRule.makeUpRule(result) + result = if (sourceRule.getParamSize() > 1) { + // get {{}} + sourceRule.rule + } else { + // 键值直接访问 + result[sourceRule.rule] + } + result?.let { + if (sourceRule.replaceRegex.isNotEmpty() && it is List<*>) { + result = it.map { o -> + replaceRegex(o.toString(), sourceRule) + } + } else if (sourceRule.replaceRegex.isNotEmpty()) { + result = replaceRegex(result.toString(), sourceRule) + } + } + } else { + for (sourceRule in ruleList) { + putRule(sourceRule.putMap) + sourceRule.makeUpRule(result) + result ?: continue + val rule = sourceRule.rule + if (rule.isNotEmpty()) { + result = when (sourceRule.mode) { + Mode.Js -> evalJS(rule, result) + Mode.Json -> getAnalyzeByJSonPath(result).getStringList(rule) + Mode.XPath -> getAnalyzeByXPath(result).getStringList(rule) + Mode.Default -> getAnalyzeByJSoup(result).getStringList(rule) + else -> rule + } + } + if (sourceRule.replaceRegex.isNotEmpty() && result is List<*>) { + val newList = ArrayList() + for (item in result) { + newList.add(replaceRegex(item.toString(), sourceRule)) + } + result = newList + } else if (sourceRule.replaceRegex.isNotEmpty()) { + result = replaceRegex(result.toString(), sourceRule) + } + } + } + } + if (result == null) return null + if (result is String) { + result = result.split("\n") + } + if (isUrl) { + val urlList = ArrayList() + if (result is List<*>) { + for (url in result) { + val absoluteURL = NetworkUtils.getAbsoluteURL(redirectUrl, url.toString()) + if (absoluteURL.isNotEmpty() && !urlList.contains(absoluteURL)) { + urlList.add(absoluteURL) + } + } + } + return urlList + } + @Suppress("UNCHECKED_CAST") + return result as? List + } + + /** + * 获取文本 + */ + @JvmOverloads + fun getString(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false): String { + if (TextUtils.isEmpty(ruleStr)) return "" + val ruleList = splitSourceRuleCacheString(ruleStr) + return getString(ruleList, mContent, isUrl) + } + + fun getString(ruleStr: String?, unescape: Boolean): String { + if (TextUtils.isEmpty(ruleStr)) return "" + val ruleList = splitSourceRuleCacheString(ruleStr) + return getString(ruleList, unescape = unescape) + } + + @JvmOverloads + fun getString( + ruleList: List, + mContent: Any? = null, + isUrl: Boolean = false, + unescape: Boolean = true + ): String { + var result: Any? = null + val content = mContent ?: this.content + if (content != null && ruleList.isNotEmpty()) { + result = content + if (result is NativeObject) { + val sourceRule = ruleList.first() + putRule(sourceRule.putMap) + sourceRule.makeUpRule(result) + result = if (sourceRule.getParamSize() > 1) { + // get {{}} + sourceRule.rule + } else { + // 键值直接访问 + result[sourceRule.rule]?.toString() + }?.let { + replaceRegex(it, sourceRule) + } + } else { + for (sourceRule in ruleList) { + putRule(sourceRule.putMap) + sourceRule.makeUpRule(result) + result ?: continue + val rule = sourceRule.rule + if (rule.isNotBlank() || sourceRule.replaceRegex.isEmpty()) { + result = when (sourceRule.mode) { + Mode.Js -> evalJS(rule, result) + Mode.Json -> getAnalyzeByJSonPath(result).getString(rule) + Mode.XPath -> getAnalyzeByXPath(result).getString(rule) + Mode.Default -> if (isUrl) { + getAnalyzeByJSoup(result).getString0(rule) + } else { + getAnalyzeByJSoup(result).getString(rule) + } + + else -> rule + } + } + if (result != null && sourceRule.replaceRegex.isNotEmpty()) { + result = replaceRegex(result.toString(), sourceRule) + } + } + } + } + if (result == null) result = "" + val resultStr = result.toString() + val str = if (unescape && resultStr.indexOf('&') > -1) { + StringEscapeUtils.unescapeHtml4(resultStr) + } else { + resultStr + } + if (isUrl) { + return if (str.isBlank()) { + baseUrl ?: "" + } else { + NetworkUtils.getAbsoluteURL(redirectUrl, str) + } + } + return str + } + + /** + * 获取Element + */ + fun getElement(ruleStr: String): Any? { + if (TextUtils.isEmpty(ruleStr)) return null + var result: Any? = null + val content = this.content + val ruleList = splitSourceRule(ruleStr, true) + if (content != null && ruleList.isNotEmpty()) { + result = content + for (sourceRule in ruleList) { + putRule(sourceRule.putMap) + sourceRule.makeUpRule(result) + result ?: continue + val rule = sourceRule.rule + result = when (sourceRule.mode) { + Mode.Regex -> AnalyzeByRegex.getElement( + result.toString(), + rule.splitNotBlank("&&") + ) + + Mode.Js -> evalJS(rule, result) + Mode.Json -> getAnalyzeByJSonPath(result).getObject(rule) + Mode.XPath -> getAnalyzeByXPath(result).getElements(rule) + else -> getAnalyzeByJSoup(result).getElements(rule) + } + if (sourceRule.replaceRegex.isNotEmpty()) { + result = replaceRegex(result.toString(), sourceRule) + } + } + } + return result + } + + /** + * 获取列表 + */ + @Suppress("UNCHECKED_CAST") + fun getElements(ruleStr: String): List { + var result: Any? = null + val content = this.content + val ruleList = splitSourceRule(ruleStr, true) + if (content != null && ruleList.isNotEmpty()) { + result = content + for (sourceRule in ruleList) { + putRule(sourceRule.putMap) + result ?: continue + val rule = sourceRule.rule + result = when (sourceRule.mode) { + Mode.Regex -> AnalyzeByRegex.getElements( + result.toString(), + rule.splitNotBlank("&&") + ) + + Mode.Js -> evalJS(rule, result) + Mode.Json -> getAnalyzeByJSonPath(result).getList(rule) + Mode.XPath -> getAnalyzeByXPath(result).getElements(rule) + else -> getAnalyzeByJSoup(result).getElements(rule) + } + } + } + result?.let { + return it as List + } + return ArrayList() + } + + /** + * 保存变量 + */ + private fun putRule(map: Map) { + for ((key, value) in map) { + put(key, getString(value)) + } + } + + /** + * 分离put规则 + */ + private fun splitPutRule(ruleStr: String, putMap: HashMap): String { + var vRuleStr = ruleStr + val putMatcher = putPattern.matcher(vRuleStr) + while (putMatcher.find()) { + vRuleStr = vRuleStr.replace(putMatcher.group(), "") + val putJsonStr = putMatcher.group(1) + val putJson = GSONStrict.fromJsonObject>(putJsonStr) + .getOrNull() + if (putJson != null) { + putMap.putAll(putJson) + continue + } + GSON.fromJsonObject>(putJsonStr) + .getOrNull() + ?.let { + if (!loggedNonStandardJSON) { + Debug.log("≡@put 规则 JSON 格式不规范,请改为规范格式") + loggedNonStandardJSON = true + } + putMap.putAll(it) + } + } + return vRuleStr + } + + /** + * 正则替换 + */ + private fun replaceRegex(result: String, rule: SourceRule): String { + if (rule.replaceRegex.isEmpty()) return result + val replaceRegex = rule.replaceRegex + val replacement = rule.replacement + val regex = compileRegexCache(replaceRegex) + if (rule.replaceFirst) { + /* ##match##replace### 获取第一个匹配到的结果并进行替换 */ + if (regex != null) kotlin.runCatching { + val pattern = regex.toPattern() + val matcher = pattern.matcher(result) + return if (matcher.find()) { + matcher.group(0)!!.replaceFirst(regex, replacement) + } else { + "" + } + } + return replacement + } else { + /* ##match##replace 替换*/ + if (regex != null) kotlin.runCatching { + return result.replace(regex, replacement) + } + return result.replace(replaceRegex, replacement) + } + } + + private fun compileRegexCache(regex: String): Regex? { + return regexCache.getOrPutLimit(regex, 16) { + try { + regex.toRegex() + } catch (e: Exception) { + null + } + } + } + + /** + * getString 类规则缓存 + */ + private fun splitSourceRuleCacheString(ruleStr: String?): List { + if (ruleStr.isNullOrEmpty()) return emptyList() + return stringRuleCache.getOrPut(ruleStr) { + splitSourceRule(ruleStr) + } + } + + /** + * 分解规则生成规则列表 + */ + fun splitSourceRule(ruleStr: String?, allInOne: Boolean = false): List { + if (ruleStr.isNullOrEmpty()) return emptyList() + val ruleList = ArrayList() + var mMode: Mode = Mode.Default + var start = 0 + //仅首字符为:时为AllInOne,其实:与伪类选择器冲突,建议改成?更合理 + if (allInOne && ruleStr.startsWith(":")) { + mMode = Mode.Regex + isRegex = true + start = 1 + } else if (isRegex) { + mMode = Mode.Regex + } + var tmp: String + val jsMatcher = JS_PATTERN.matcher(ruleStr) + while (jsMatcher.find()) { + if (jsMatcher.start() > start) { + tmp = ruleStr.substring(start, jsMatcher.start()).trim { it <= ' ' } + if (tmp.isNotEmpty()) { + ruleList.add(SourceRule(tmp, mMode)) + } + } + ruleList.add(SourceRule(jsMatcher.group(2) ?: jsMatcher.group(1), Mode.Js)) + start = jsMatcher.end() + } + + if (ruleStr.length > start) { + tmp = ruleStr.substring(start).trim { it <= ' ' } + if (tmp.isNotEmpty()) { + ruleList.add(SourceRule(tmp, mMode)) + } + } + + return ruleList + } + + private fun getOrCreateSingleSourceRule(rule: String): List { + return stringRuleCache.getOrPutLimit(rule, 16) { + listOf(SourceRule(rule)) + } + } + + /** + * 规则类 + */ + inner class SourceRule internal constructor( + ruleStr: String, + internal var mode: Mode = Mode.Default + ) { + internal var rule: String + internal var replaceRegex = "" + internal var replacement = "" + internal var replaceFirst = false + internal val putMap = HashMap() + private val ruleParam = ArrayList() + private val ruleType = ArrayList() + private val getRuleType = -2 + private val jsRuleType = -1 + private val defaultRuleType = 0 + + init { + rule = when { + mode == Mode.Js || mode == Mode.Regex -> ruleStr + ruleStr.startsWith("@CSS:", true) -> { + mode = Mode.Default + ruleStr + } + + ruleStr.startsWith("@@") -> { + mode = Mode.Default + ruleStr.substring(2) + } + + ruleStr.startsWith("@XPath:", true) -> { + mode = Mode.XPath + ruleStr.substring(7) + } + + ruleStr.startsWith("@Json:", true) -> { + mode = Mode.Json + ruleStr.substring(6) + } + + isJSON || ruleStr.startsWith("$.") || ruleStr.startsWith("$[") -> { + mode = Mode.Json + ruleStr + } + + ruleStr.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头 + mode = Mode.XPath + ruleStr + } + + else -> ruleStr + } + //分离put + rule = splitPutRule(rule, putMap) + //@get,{{ }}, 拆分 + var start = 0 + var tmp: String + val evalMatcher = evalPattern.matcher(rule) + + if (evalMatcher.find()) { + tmp = rule.substring(start, evalMatcher.start()) + if (mode != Mode.Js && mode != Mode.Regex && + (evalMatcher.start() == 0 || !tmp.contains("##")) + ) { + mode = Mode.Regex + } + do { + if (evalMatcher.start() > start) { + tmp = rule.substring(start, evalMatcher.start()) + splitRegex(tmp) + } + tmp = evalMatcher.group() + when { + tmp.startsWith("@get:", true) -> { + ruleType.add(getRuleType) + ruleParam.add(tmp.substring(6, tmp.lastIndex)) + } + + tmp.startsWith("{{") -> { + ruleType.add(jsRuleType) + ruleParam.add(tmp.substring(2, tmp.length - 2)) + } + + else -> { + splitRegex(tmp) + } + } + start = evalMatcher.end() + } while (evalMatcher.find()) + } + if (rule.length > start) { + tmp = rule.substring(start) + splitRegex(tmp) + } + } + + /** + * 拆分\$\d{1,2} + */ + private fun splitRegex(ruleStr: String) { + var start = 0 + var tmp: String + val ruleStrArray = ruleStr.split("##") + val regexMatcher = regexPattern.matcher(ruleStrArray[0]) + + if (regexMatcher.find()) { + if (mode != Mode.Js && mode != Mode.Regex) { + mode = Mode.Regex + } + do { + if (regexMatcher.start() > start) { + tmp = ruleStr.substring(start, regexMatcher.start()) + ruleType.add(defaultRuleType) + ruleParam.add(tmp) + } + tmp = regexMatcher.group() + ruleType.add(tmp.substring(1).toInt()) + ruleParam.add(tmp) + start = regexMatcher.end() + } while (regexMatcher.find()) + } + if (ruleStr.length > start) { + tmp = ruleStr.substring(start) + ruleType.add(defaultRuleType) + ruleParam.add(tmp) + } + } + + /** + * 替换@get,{{ }} + */ + fun makeUpRule(result: Any?) { + val infoVal = StringBuilder() + if (ruleParam.isNotEmpty()) { + var index = ruleParam.size + while (index-- > 0) { + val regType = ruleType[index] + when { + regType > defaultRuleType -> { + @Suppress("UNCHECKED_CAST") + (result as? List)?.run { + if (this.size > regType) { + this[regType]?.let { + infoVal.insert(0, it) + } + } + } ?: infoVal.insert(0, ruleParam[index]) + } + + regType == jsRuleType -> { + if (isRule(ruleParam[index])) { + val ruleList = getOrCreateSingleSourceRule(ruleParam[index]) + getString(ruleList).let { + infoVal.insert(0, it) + } + } else { + val jsEval: Any? = evalJS(ruleParam[index], result) + when { + jsEval == null -> Unit + jsEval is String -> infoVal.insert(0, jsEval) + jsEval is Double && jsEval % 1.0 == 0.0 -> infoVal.insert( + 0, + String.format(Locale.ROOT, "%.0f", jsEval) + ) + + else -> infoVal.insert(0, jsEval.toString()) + } + } + } + + regType == getRuleType -> { + infoVal.insert(0, get(ruleParam[index])) + } + + else -> infoVal.insert(0, ruleParam[index]) + } + } + rule = infoVal.toString() + } + //分离正则表达式 + val ruleStrS = rule.split("##") + rule = ruleStrS[0].trim() + if (ruleStrS.size > 1) { + replaceRegex = ruleStrS[1] + } + if (ruleStrS.size > 2) { + replacement = ruleStrS[2] + } + if (ruleStrS.size > 3) { + replaceFirst = true + } + } + + private fun isRule(ruleStr: String): Boolean { + return ruleStr.startsWith('@') //js首个字符不可能是@,除非是装饰器,所以@开头规定为规则 + || ruleStr.startsWith("$.") + || ruleStr.startsWith("$[") + || ruleStr.startsWith("//") + } + + fun getParamSize(): Int { + return ruleParam.size + } + } + + enum class Mode { + XPath, Json, Default, Js, Regex + } + + /** + * 保存数据 + */ + fun put(key: String, value: String): String { + if (key == "bookName" || key == "title") { + Debug.log("≡变量 $key 在特定情况下会被覆盖,建议使用其他键名") + } + chapter?.putVariable(key, value) + ?: book?.putVariable(key, value) + ?: ruleData?.putVariable(key, value) + ?: source?.put(key, value) + return value + } + + /** + * 获取保存的数据 + */ + fun get(key: String): String { + when (key) { + "bookName" -> book?.let { + return it.name + } + + "title" -> chapter?.let { + return it.title + } + } + return chapter?.getVariable(key)?.takeIf { it.isNotEmpty() } + ?: book?.getVariable(key)?.takeIf { it.isNotEmpty() } + ?: ruleData?.getVariable(key)?.takeIf { it.isNotEmpty() } + ?: source?.get(key)?.takeIf { it.isNotEmpty() } + ?: "" + } + + /** + * 执行JS + */ + fun evalJS(jsStr: String, result: Any? = null): Any? { + val bindings = buildScriptBindings { bindings -> + bindings["java"] = this + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + bindings["source"] = source + bindings["book"] = book + bindings["result"] = result + bindings["baseUrl"] = baseUrl + bindings["chapter"] = chapter + bindings["title"] = chapter?.title + bindings["src"] = content + bindings["nextChapterUrl"] = nextChapterUrl + bindings["rssArticle"] = rssArticle + } + val topScope = source?.getShareScope(coroutineContext) ?: topScopeRef?.get() + val scope = if (topScope == null) { + RhinoScriptEngine.getRuntimeScope(bindings).apply { + if (evalJSCallCount++ > 16) { + topScopeRef = WeakReference(prototype) + } + } + } else { + bindings.apply { + prototype = topScope + } + } + val script = compileScriptCache(jsStr) + val result = script.eval(scope, coroutineContext) + return result + } + + private fun compileScriptCache(jsStr: String): CompiledScript { + return scriptCache.getOrPutLimit(jsStr, 16) { + RhinoScriptEngine.compile(jsStr) + } + } + + override fun getSource(): BaseSource? { + return source + } + + /** + * js实现跨域访问,不能删 + */ + override fun ajax(url: Any): String? { + val urlStr = if (url is List<*>) { + url.firstOrNull().toString() + } else { + url.toString() + } + val analyzeUrl = AnalyzeUrl( + urlStr, + source = source, + ruleData = book, + coroutineContext = coroutineContext + ) + return kotlin.runCatching { + analyzeUrl.getStrResponse().body + }.onFailure { + coroutineContext.ensureActive() + log("ajax(${urlStr}) error\n${it.stackTraceToString()}") + it.printOnDebug() + }.getOrElse { + it.stackTraceStr + } + } + + /** + * 重新获取book + */ + fun reGetBook() { + if (!preUpdateJs) throw NoStackTraceException("只能在 preUpdateJs 中调用") + val bookSource = source as? BookSource + val book = book as? Book + if (bookSource == null || book == null) return + runBlocking(coroutineContext) { + withTimeout(1800000) { + WebBook.preciseSearchAwait(bookSource, book.name, book.author) + .getOrThrow().let { + book.bookUrl = it.bookUrl + it.variableMap.forEach { entry -> + book.putVariable(entry.key, entry.value) + } + } + WebBook.getBookInfoAwait(bookSource, book, false) + } + } + } + + /** + * 更新tocUrl,有些书源目录url定期更新,可以在js调用更新 + */ + fun refreshTocUrl() { + if (!preUpdateJs) throw NoStackTraceException("只能在 preUpdateJs 中调用") + val bookSource = source as? BookSource + val book = book as? Book + if (bookSource == null || book == null) return + runBlocking(coroutineContext) { + withTimeout(1800000) { + WebBook.getBookInfoAwait(bookSource, book, false) + } + } + } + + companion object { + private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE) + private val evalPattern = + Pattern.compile("@get:\\{[^}]+?\\}|\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE) + private val regexPattern = Pattern.compile("\\$\\d{1,2}") + + fun AnalyzeRule.setCoroutineContext(context: CoroutineContext): AnalyzeRule { + coroutineContext = context.minusKey(ContinuationInterceptor) + return this + } + + fun AnalyzeRule.setRuleData(ruleData: RuleDataInterface?): AnalyzeRule { + this.ruleData = ruleData + return this + } + + fun AnalyzeRule.setNextChapterUrl(nextChapterUrl: String?): AnalyzeRule { + this.nextChapterUrl = nextChapterUrl + return this + } + + fun AnalyzeRule.setChapter(chapter: BookChapter?): AnalyzeRule { + this.chapter = chapter + return this + } + + } + +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt new file mode 100755 index 000000000..5706cfb4d --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -0,0 +1,851 @@ +package io.legado.app.model.analyzeRule + +import android.annotation.SuppressLint +import android.util.Base64 +import androidx.annotation.Keep +import androidx.media3.common.MediaItem +import cn.hutool.core.codec.PercentCodec +import cn.hutool.core.net.RFC3986 +import cn.hutool.core.util.HexUtil +import com.bumptech.glide.load.model.GlideUrl +import com.script.buildScriptBindings +import com.script.rhino.RhinoScriptEngine +import com.script.rhino.runScriptWithContext +import io.legado.app.constant.AppConst.UA_NAME +import io.legado.app.constant.AppPattern +import io.legado.app.constant.AppPattern.JS_PATTERN +import io.legado.app.constant.AppPattern.dataUriRegex +import io.legado.app.data.entities.BaseSource +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.help.CacheManager +import io.legado.app.help.ConcurrentRateLimiter +import io.legado.app.help.JsExtensions +import io.legado.app.help.config.AppConfig +import io.legado.app.help.exoplayer.ExoPlayerHelper +import io.legado.app.help.glide.GlideHeaders +import io.legado.app.help.http.BackstageWebView +import io.legado.app.help.http.CookieManager +import io.legado.app.help.http.CookieManager.mergeCookies +import io.legado.app.help.http.CookieStore +import io.legado.app.help.http.RequestMethod +import io.legado.app.help.http.StrResponse +import io.legado.app.help.http.addHeaders +import io.legado.app.help.http.get +import io.legado.app.help.http.getProxyClient +import io.legado.app.help.http.newCallResponse +import io.legado.app.help.http.newCallStrResponse +import io.legado.app.help.http.postForm +import io.legado.app.help.http.postJson +import io.legado.app.help.http.postMultipart +import io.legado.app.help.source.getShareScope +import io.legado.app.model.Debug +import io.legado.app.utils.EncoderUtils +import io.legado.app.utils.GSON +import io.legado.app.utils.GSONStrict +import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.fromJsonObject +import io.legado.app.utils.get +import io.legado.app.utils.isJson +import io.legado.app.utils.isJsonArray +import io.legado.app.utils.isJsonObject +import io.legado.app.utils.isXml +import kotlinx.coroutines.runBlocking +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.net.URLEncoder +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.max + +/** + * Created by GKF on 2018/1/24. + * 搜索URL规则解析 + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +@Keep +@SuppressLint("DefaultLocale") +class AnalyzeUrl( + private val mUrl: String, + private val key: String? = null, + private val page: Int? = null, + private val speakText: String? = null, + private val speakSpeed: Int? = null, + private var baseUrl: String = "", + private val source: BaseSource? = null, + private val ruleData: RuleDataInterface? = null, + private val chapter: BookChapter? = null, + private val readTimeout: Long? = null, + private val callTimeout: Long? = null, + private var coroutineContext: CoroutineContext = EmptyCoroutineContext, + headerMapF: Map? = null, + hasLoginHeader: Boolean = true +) : JsExtensions { + + var ruleUrl = "" + private set + var url: String = "" + private set + var type: String? = null + private set + val headerMap = LinkedHashMap() + private var body: String? = null + private var urlNoQuery: String = "" + private var encodedForm: String? = null + private var encodedQuery: String? = null + private var charset: String? = null + private var method = RequestMethod.GET + private var proxy: String? = null + private var retry: Int = 0 + private var useWebView: Boolean = false + private var webJs: String? = null + private val enabledCookieJar = source?.enabledCookieJar == true + private val domain: String + private var webViewDelayTime: Long = 0 + private val concurrentRateLimiter = ConcurrentRateLimiter(source) + + // 服务器ID + var serverID: Long? = null + private set + + init { + coroutineContext = coroutineContext.minusKey(ContinuationInterceptor) + val urlMatcher = paramPattern.matcher(baseUrl) + if (urlMatcher.find()) baseUrl = baseUrl.substring(0, urlMatcher.start()) + (headerMapF ?: runScriptWithContext(coroutineContext) { + source?.getHeaderMap(hasLoginHeader) + })?.let { + headerMap.putAll(it) + if (it.containsKey("proxy")) { + proxy = it["proxy"] + headerMap.remove("proxy") + } + } + initUrl() + domain = NetworkUtils.getSubDomain(source?.getKey() ?: url) + } + + /** + * 处理url + */ + fun initUrl() { + ruleUrl = mUrl + //执行@js, + analyzeJs() + //替换参数 + replaceKeyPageJs() + //处理URL + analyzeUrl() + } + + /** + * 执行@js, + */ + private fun analyzeJs() { + var start = 0 + val jsMatcher = JS_PATTERN.matcher(ruleUrl) + var result = ruleUrl + while (jsMatcher.find()) { + if (jsMatcher.start() > start) { + ruleUrl.substring(start, jsMatcher.start()).trim().let { + if (it.isNotEmpty()) { + result = it.replace("@result", result) + } + } + } + result = evalJS(jsMatcher.group(2) ?: jsMatcher.group(1), result).toString() + start = jsMatcher.end() + } + if (ruleUrl.length > start) { + ruleUrl.substring(start).trim().let { + if (it.isNotEmpty()) { + result = it.replace("@result", result) + } + } + } + ruleUrl = result + } + + /** + * 替换关键字,页数,JS + */ + private fun replaceKeyPageJs() { //先替换内嵌规则再替换页数规则,避免内嵌规则中存在大于小于号时,规则被切错 + //js + if (ruleUrl.contains("{{") && ruleUrl.contains("}}")) { + val analyze = RuleAnalyzer(ruleUrl) //创建解析 + //替换所有内嵌{{js}} + val url = analyze.innerRule("{{", "}}") { + val jsEval = evalJS(it) ?: "" + when { + jsEval is String -> jsEval + jsEval is Double && jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval) + else -> jsEval.toString() + } + } + if (url.isNotEmpty()) ruleUrl = url + } + //page + page?.let { + val matcher = pagePattern.matcher(ruleUrl) + while (matcher.find()) { + val pages = matcher.group(1)!!.split(",") + ruleUrl = if (page < pages.size) { //pages[pages.size - 1]等同于pages.last() + ruleUrl.replace(matcher.group(), pages[page - 1].trim { it <= ' ' }) + } else { + ruleUrl.replace(matcher.group(), pages.last().trim { it <= ' ' }) + } + } + } + } + + /** + * 解析Url + */ + private fun analyzeUrl() { + //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。 + val urlMatcher = paramPattern.matcher(ruleUrl) + val urlNoOption = + if (urlMatcher.find()) ruleUrl.substring(0, urlMatcher.start()) else ruleUrl + url = NetworkUtils.getAbsoluteURL(baseUrl, urlNoOption) + NetworkUtils.getBaseUrl(url)?.let { + baseUrl = it + } + if (urlNoOption.length != ruleUrl.length) { + val urlOptionStr = ruleUrl.substring(urlMatcher.end()) + var urlOption = GSONStrict.fromJsonObject(urlOptionStr).getOrNull() + if (urlOption == null) { + urlOption = GSON.fromJsonObject(urlOptionStr).getOrNull() + if (urlOption != null) { + log("链接参数 JSON 格式不规范,请改为规范格式") + } + } + urlOption?.let { option -> + option.getMethod()?.let { + if (it.equals("POST", true)) method = RequestMethod.POST + } + option.getHeaderMap()?.forEach { entry -> + headerMap[entry.key.toString()] = entry.value.toString() + } + option.getBody()?.let { + body = it + } + type = option.getType() + charset = option.getCharset() + retry = option.getRetry() + useWebView = option.useWebView() + webJs = option.getWebJs() + option.getJs()?.let { jsStr -> + evalJS(jsStr, url)?.toString()?.let { + url = it + } + } + serverID = option.getServerID() + webViewDelayTime = max(0, option.getWebViewDelayTime() ?: 0) + } + } + urlNoQuery = url + when (method) { + RequestMethod.GET -> { + val pos = url.indexOf('?') + if (pos != -1) { + analyzeQuery(url.substring(pos + 1)) + urlNoQuery = url.substring(0, pos) + } + } + + RequestMethod.POST -> body?.let { + if (!it.isJson() && !it.isXml() && headerMap["Content-Type"].isNullOrEmpty()) { + analyzeFields(it) + } + } + } + } + + /** + * 解析QueryMap = + * name= + * name=name + * name= eg name=bmFtZQ== + */ + private fun analyzeFields(fieldsTxt: String) { + encodedForm = encodeParams(fieldsTxt, charset, false) + } + + private fun analyzeQuery(query: String) { + encodedQuery = encodeParams(query, charset, true) + } + + private fun encodeParams(params: String, charset: String?, isQuery: Boolean): String { + val checkEncoded = charset.isNullOrEmpty() + val charset = when { + charset.isNullOrEmpty() -> Charsets.UTF_8 + charset == "escape" -> null + else -> charset(charset) + } + if (isQuery && charset != null) { + if (NetworkUtils.encodedQuery(params)) { + return params + } + return queryEncoder.encode(params, charset) + } + val len = params.length + val sb = StringBuilder() + var pos = 0 + while (pos <= len) { + if (sb.isNotEmpty()) { + sb.append("&") + } + var ampOffset = params.indexOf("&", pos) + if (ampOffset == -1) { + ampOffset = len + } + val eqOffset = params.indexOf("=", pos) + val key: String + val value: String? + if (eqOffset == -1 || eqOffset > ampOffset) { + key = params.substring(pos, ampOffset) + value = null + } else { + key = params.substring(pos, eqOffset) + value = params.substring(eqOffset + 1, ampOffset) + } + sb.appendEncoded(key, checkEncoded, charset) + if (value != null) { + sb.append("=") + sb.appendEncoded(value, checkEncoded, charset) + } + pos = ampOffset + 1 + } + return sb.toString() + } + + private fun StringBuilder.appendEncoded( + value: String, + checkEncoded: Boolean, + charset: Charset? + ) { + if (checkEncoded && NetworkUtils.encodedForm(value)) { + append(value) + } else if (charset == null) { + append(EncoderUtils.escape(value)) + } else { + append(URLEncoder.encode(value, charset)) + } + } + + /** + * 执行JS + */ + fun evalJS(jsStr: String, result: Any? = null): Any? { + val bindings = buildScriptBindings { bindings -> + bindings["java"] = this + bindings["baseUrl"] = baseUrl + bindings["cookie"] = CookieStore + bindings["cache"] = CacheManager + bindings["page"] = page + bindings["key"] = key + bindings["speakText"] = speakText + bindings["speakSpeed"] = speakSpeed + bindings["book"] = ruleData as? Book + bindings["source"] = source + bindings["result"] = result + } + val sharedScope = source?.getShareScope(coroutineContext) + val scope = if (sharedScope == null) { + RhinoScriptEngine.getRuntimeScope(bindings) + } else { + bindings.apply { + prototype = sharedScope + } + } + return RhinoScriptEngine.eval(jsStr, scope, coroutineContext) + } + + fun put(key: String, value: String): String { + if (key == "bookName" || key == "title") { + Debug.log("≡变量 $key 在特定情况下会被覆盖,建议使用其他键名") + } + chapter?.putVariable(key, value) + ?: ruleData?.putVariable(key, value) + return value + } + + fun get(key: String): String { + when (key) { + "bookName" -> (ruleData as? Book)?.let { + return it.name + } + + "title" -> chapter?.let { + return it.title + } + } + return chapter?.getVariable(key)?.takeIf { it.isNotEmpty() } + ?: ruleData?.getVariable(key)?.takeIf { it.isNotEmpty() } + ?: "" + } + + /** + * 访问网站,返回StrResponse + */ + suspend fun getStrResponseAwait( + jsStr: String? = null, + sourceRegex: String? = null, + useWebView: Boolean = true, + ): StrResponse { + if (type != null) { + return StrResponse(url, HexUtil.encodeHexStr(getByteArrayAwait())) + } + concurrentRateLimiter.withLimit { + setCookie() + val strResponse: StrResponse + if (this.useWebView && useWebView) { + strResponse = when (method) { + RequestMethod.POST -> { + val res = getClient().newCallStrResponse(retry) { + addHeaders(headerMap) + url(urlNoQuery) + if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) { + postForm(encodedForm ?: "") + } else { + postJson(body) + } + } + BackstageWebView( + url = res.url, + html = res.body, + tag = source?.getKey(), + javaScript = webJs ?: jsStr, + sourceRegex = sourceRegex, + headerMap = headerMap, + delayTime = webViewDelayTime + ).getStrResponse() + } + + else -> BackstageWebView( + url = url, + tag = source?.getKey(), + javaScript = webJs ?: jsStr, + sourceRegex = sourceRegex, + headerMap = headerMap, + delayTime = webViewDelayTime + ).getStrResponse() + } + } else { + strResponse = getClient().newCallStrResponse(retry) { + addHeaders(headerMap) + when (method) { + RequestMethod.POST -> { + url(urlNoQuery) + val contentType = headerMap["Content-Type"] + val body = body + if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) { + postForm(encodedForm ?: "") + } else if (!contentType.isNullOrBlank()) { + val requestBody = body.toRequestBody(contentType.toMediaType()) + post(requestBody) + } else { + postJson(body) + } + } + + else -> get(urlNoQuery, encodedQuery) + } + }.let { + val isXml = it.raw.body.contentType()?.toString() + ?.matches(AppPattern.xmlContentTypeRegex) == true + if (isXml && it.body?.trim()?.startsWith("" + it.body) + } else it + } + } + return strResponse + } + } + + @JvmOverloads + fun getStrResponse( + jsStr: String? = null, + sourceRegex: String? = null, + useWebView: Boolean = true, + ): StrResponse { + return runBlocking(coroutineContext) { + getStrResponseAwait(jsStr, sourceRegex, useWebView) + } + } + + /** + * 访问网站,返回Response + */ + suspend fun getResponseAwait(): Response { + concurrentRateLimiter.withLimit { + setCookie() + val response = getClient().newCallResponse(retry) { + addHeaders(headerMap) + when (method) { + RequestMethod.POST -> { + url(urlNoQuery) + val contentType = headerMap["Content-Type"] + val body = body + if (!encodedForm.isNullOrEmpty() || body.isNullOrBlank()) { + postForm(encodedForm ?: "") + } else if (!contentType.isNullOrBlank()) { + val requestBody = body.toRequestBody(contentType.toMediaType()) + post(requestBody) + } else { + postJson(body) + } + } + + else -> get(urlNoQuery, encodedQuery) + } + } + return response + } + } + + private fun getClient(): OkHttpClient { + val client = getProxyClient(proxy) + if (readTimeout == null && callTimeout == null) { + return client + } + return client.newBuilder().run { + if (readTimeout != null) { + readTimeout(readTimeout, TimeUnit.MILLISECONDS) + callTimeout(max(60 * 1000L, readTimeout * 2), TimeUnit.MILLISECONDS) + } + if (callTimeout != null) { + callTimeout(callTimeout, TimeUnit.MILLISECONDS) + } + build() + } + } + + fun getResponse(): Response { + return runBlocking(coroutineContext) { + getResponseAwait() + } + } + + private fun getByteArrayIfDataUri(): ByteArray? { + if (!urlNoQuery.startsWith("data:")) { + return null + } + val dataUriFindResult = dataUriRegex.find(urlNoQuery) + if (dataUriFindResult != null) { + val dataUriBase64 = dataUriFindResult.groupValues[1] + val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT) + return byteArray + } + return null + } + + /** + * 访问网站,返回ByteArray + */ + suspend fun getByteArrayAwait(): ByteArray { + getByteArrayIfDataUri()?.let { + return it + } + return getResponseAwait().body.bytes() + } + + fun getByteArray(): ByteArray { + return runBlocking(coroutineContext) { + getByteArrayAwait() + } + } + + /** + * 访问网站,返回InputStream + */ + suspend fun getInputStreamAwait(): InputStream { + getByteArrayIfDataUri()?.let { + return ByteArrayInputStream(it) + } + return getResponseAwait().body.byteStream() + } + + fun getInputStream(): InputStream { + return runBlocking(coroutineContext) { + getInputStreamAwait() + } + } + + /** + * 上传文件 + */ + suspend fun upload(fileName: String, file: Any, contentType: String): StrResponse { + return getProxyClient(proxy).newCallStrResponse(retry) { + url(urlNoQuery) + val bodyMap = GSON.fromJsonObject>(body).getOrNull()!! + bodyMap.forEach { entry -> + if (entry.value.toString() == "fileRequest") { + bodyMap[entry.key] = mapOf( + Pair("fileName", fileName), + Pair("file", file), + Pair("contentType", contentType) + ) + } + } + postMultipart(type, bodyMap) + } + } + + /** + * 设置cookie 优先级 + * urlOption临时cookie > 数据库cookie + */ + private fun setCookie() { + val cookie = kotlin.run { + /* 每次调用getXX cookieJar已经保存过了 + if (enabledCookieJar) { + val key = "${domain}_cookieJar" + CacheManager.getFromMemory(key)?.let { + return@run it + } + } + */ + CookieStore.getCookie(domain) + } + if (cookie.isNotEmpty()) { + mergeCookies(cookie, headerMap["Cookie"])?.let { + headerMap.put("Cookie", it) + } + } + if (enabledCookieJar) { + headerMap[CookieManager.cookieJarHeader] = "1" + } else { + headerMap.remove(CookieManager.cookieJarHeader) + } + } + + /** + * 保存cookieJar中的cookie在访问结束时就保存,不等到下次访问 + */ + private fun saveCookie() { + //书源启用保存cookie时 添加内存中的cookie到数据库 + if (enabledCookieJar) { + val key = "${domain}_cookieJar" + CacheManager.getFromMemory(key)?.let { + if (it is String) { + CookieStore.replaceCookie(domain, it) + CacheManager.deleteMemory(key) + } + } + } + } + + /** + *获取处理过阅读定义的urlOption和cookie的GlideUrl + */ + fun getGlideUrl(): GlideUrl { + setCookie() + return GlideUrl(url, GlideHeaders(headerMap)) + } + + fun getUserAgent(): String { + return headerMap.get(UA_NAME, true) ?: AppConfig.userAgent + } + + fun isPost(): Boolean { + return method == RequestMethod.POST + } + + override fun getSource(): BaseSource? { + return source + } + + companion object { + val paramPattern: Pattern = Pattern.compile("\\s*,\\s*(?=\\{)") + private val pagePattern = Pattern.compile("<(.*?)>") + private val queryEncoder = + RFC3986.UNRESERVED.orNew(PercentCodec.of("!$%&()*+,/:;=?@[\\]^`{|}")) + + fun AnalyzeUrl.getMediaItem(): MediaItem { + setCookie() + return ExoPlayerHelper.createMediaItem(url, headerMap) + } + + } + + @Keep + data class UrlOption( + private var method: String? = null, + private var charset: String? = null, + private var headers: Any? = null, + private var body: Any? = null, + /** + * 源Url + **/ + private var origin: String? = null, + /** + * 重试次数 + **/ + private var retry: Int? = null, + /** + * 类型 + **/ + private var type: String? = null, + /** + * 是否使用webView + **/ + private var webView: Any? = null, + /** + * webView中执行的js + **/ + private var webJs: String? = null, + /** + * 解析完url参数时执行的js + * 执行结果会赋值给url + */ + private var js: String? = null, + /** + * 服务器id + */ + private var serverID: Long? = null, + /** + * webview等待页面加载完毕的延迟时间(毫秒) + */ + private var webViewDelayTime: Long? = null, + ) { + fun setMethod(value: String?) { + method = if (value.isNullOrBlank()) null else value + } + + fun getMethod(): String? { + return method + } + + fun setCharset(value: String?) { + charset = if (value.isNullOrBlank()) null else value + } + + fun getCharset(): String? { + return charset + } + + fun setOrigin(value: String?) { + origin = if (value.isNullOrBlank()) null else value + } + + fun getOrigin(): String? { + return origin + } + + fun setRetry(value: String?) { + retry = if (value.isNullOrEmpty()) null else value.toIntOrNull() + } + + fun getRetry(): Int { + return retry ?: 0 + } + + fun setType(value: String?) { + type = if (value.isNullOrBlank()) null else value + } + + fun getType(): String? { + return type + } + + fun useWebView(): Boolean { + return when (webView) { + null, "", false, "false" -> false + else -> true + } + } + + fun useWebView(boolean: Boolean) { + webView = if (boolean) true else null + } + + fun setHeaders(value: String?) { + headers = if (value.isNullOrBlank()) { + null + } else { + GSON.fromJsonObject>(value).getOrNull() + } + } + + fun getHeaderMap(): Map<*, *>? { + return when (val value = headers) { + is Map<*, *> -> value + is String -> GSON.fromJsonObject>(value).getOrNull() + else -> null + } + } + + fun setBody(value: String?) { + body = when { + value.isNullOrBlank() -> null + value.isJsonObject() -> GSON.fromJsonObject>(value).getOrNull() + value.isJsonArray() -> GSON.fromJsonArray>(value).getOrNull() + else -> value + } + } + + fun getBody(): String? { + return body?.let { + it as? String ?: GSON.toJson(it) + } + } + + fun setWebJs(value: String?) { + webJs = if (value.isNullOrBlank()) null else value + } + + fun getWebJs(): String? { + return webJs + } + + fun setJs(value: String?) { + js = if (value.isNullOrBlank()) null else value + } + + fun getJs(): String? { + return js + } + + fun setServerID(value: String?) { + serverID = if (value.isNullOrBlank()) null else value.toLong() + } + + fun getServerID(): Long? { + return serverID + } + + fun setWebViewDelayTime(value: String?) { + webViewDelayTime = if (value.isNullOrBlank()) null else value.toLong() + } + + fun getWebViewDelayTime(): Long? { + return webViewDelayTime + } + } + + data class ConcurrentRecord( + /** + * 是否按频率 + */ + val isConcurrent: Boolean, + /** + * 开始访问时间 + */ + var time: Long, + /** + * 正在访问的个数 + */ + var frequency: Int + ) + +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/CustomUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/CustomUrl.kt new file mode 100755 index 000000000..99fe118ea --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/CustomUrl.kt @@ -0,0 +1,49 @@ +package io.legado.app.model.analyzeRule + +import io.legado.app.utils.GSON +import io.legado.app.utils.fromJsonObject + +@Suppress("unused") +class CustomUrl(url: String) { + + private val mUrl: String + private val attribute = hashMapOf() + + init { + val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) + mUrl = if (urlMatcher.find()) { + val attr = url.substring(urlMatcher.end()) + GSON.fromJsonObject>(attr).getOrNull()?.let { + attribute.putAll(it) + } + url.substring(0, urlMatcher.start()) + } else { + url + } + } + + fun putAttribute(key: String, value: Any?): CustomUrl { + if (value == null) { + attribute.remove(key) + } else { + attribute[key] = value + } + return this + } + + fun getUrl(): String { + return mUrl + } + + fun getAttr(): Map { + return attribute + } + + override fun toString(): String { + if (attribute.isEmpty()) { + return mUrl + } + return mUrl + "," + GSON.toJson(attribute) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java b/app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java new file mode 100755 index 000000000..c5b9695c7 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/analyzeRule/QueryTTF.java @@ -0,0 +1,1055 @@ +package io.legado.app.model.analyzeRule; + +import androidx.annotation.Keep; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedList; + +@Keep +@SuppressWarnings({"FieldCanBeLocal", "unused"}) +public class QueryTTF { + /** + * 文件头 + * + * @url
    Microsoft opentype 字体文档 + */ + private static class Header { + /** + * uint32 字体版本 0x00010000 (ttf) + */ + public long sfntVersion; + /** + * uint16 Number of tables. + */ + public int numTables; + /** + * uint16 + */ + public int searchRange; + /** + * uint16 + */ + public int entrySelector; + /** + * uint16 + */ + public int rangeShift; + } + + /** + * 数据表目录 + */ + private static class Directory { + /** + * uint32 (表标识符) + */ + public String tableTag; + /** + * uint32 (该表的校验和) + */ + public int checkSum; + /** + * uint32 (TTF文件 Bytes 数据索引 0 开始的偏移地址) + */ + public int offset; + /** + * uint32 (该表的长度) + */ + public int length; + } + + private static class NameLayout { + public int format; + public int count; + public int stringOffset; + public LinkedList records = new LinkedList<>(); + } + + private static class NameRecord { + public int platformID; // 平台标识符<0:Unicode, 1:Mac, 2:ISO, 3:Windows, 4:Custom> + public int encodingID; // 编码标识符 + public int languageID; // 语言标识符 + public int nameID; // 名称标识符 + public int length; // 名称字符串的长度 + public int offset; // 名称字符串相对于stringOffset的字节偏移量 + } + + /** + * Font Header Table + */ + private static class HeadLayout { + /** + * uint16 + */ + public int majorVersion; + /** + * uint16 + */ + public int minorVersion; + /** + * uint16 + */ + public int fontRevision; + /** + * uint32 + */ + public int checkSumAdjustment; + /** + * uint32 + */ + public int magicNumber; + /** + * uint16 + */ + public int flags; + /** + * uint16 + */ + public int unitsPerEm; + /** + * long + */ + public long created; + /** + * long + */ + public long modified; + /** + * int16 + */ + public short xMin; + /** + * int16 + */ + public short yMin; + /** + * int16 + */ + public short xMax; + /** + * int16 + */ + public short yMax; + /** + * uint16 + */ + public int macStyle; + /** + * uint16 + */ + public int lowestRecPPEM; + /** + * int16 + */ + public short fontDirectionHint; + /** + * int16 + *

    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 literal 0 HcmV?d00001 diff --git a/app/download.gradle b/app/download.gradle new file mode 100755 index 000000000..604ac72fe --- /dev/null +++ b/app/download.gradle @@ -0,0 +1,112 @@ +import java.security.MessageDigest + +apply plugin: 'de.undercouch.download' + +def BASE_PATH = "https://storage.googleapis.com/chromium-cronet/android/" + CronetVersion + "/Release/cronet/" +def assetsDir = projectDir.toString() + "/src/main/assets" +def libPath = projectDir.toString() + "/cronetlib" +def soPath = projectDir.toString() + "/so" + +/** + * 从文件生成MD5 + * @param file + * @return + */ +static def generateMD5(final file) { + MessageDigest digest = MessageDigest.getInstance("MD5") + file.withInputStream() { is -> + byte[] buffer = new byte[1024] + //noinspection GroovyUnusedAssignment + int numRead = 0 + while ((numRead = is.read(buffer)) > 0) { + digest.update(buffer, 0, numRead) + } + } + return String.format("%032x", new BigInteger(1, digest.digest())).toLowerCase() +} + +/** + * 下载Cronet相关的jar + */ +tasks.register('downloadJar', Download) { + src([ + BASE_PATH + "cronet_api.jar", + BASE_PATH + "cronet_impl_common_java.jar", + BASE_PATH + "cronet_impl_native_java.jar", + BASE_PATH + "cronet_impl_platform_java.jar", + BASE_PATH + "cronet_shared_java.jar" + ]) + dest libPath + overwrite true + onlyIfModified false +} +/** + * 下载Cronet的arm64-v8a so + */ +tasks.register('downloadARM64', Download) { + src BASE_PATH + "libs/arm64-v8a/libcronet." + CronetVersion + ".so" + dest soPath + "/arm64-v8a.so" + overwrite true + onlyIfModified true +} +/** + * 下载Cronet的armeabi-v7a so + */ +tasks.register('downloadARMv7', Download) { + src BASE_PATH + "libs/armeabi-v7a/libcronet." + CronetVersion + ".so" + dest soPath + "/armeabi-v7a.so" + overwrite true + onlyIfModified true +} +/** + * 下载Cronet的x86_64 so + */ +tasks.register('downloadX86_64', Download) { + src BASE_PATH + "libs/x86_64/libcronet." + CronetVersion + ".so" + dest soPath + "/x86_64.so" + overwrite true + onlyIfModified true +} +/** + * 下载Cronet的x86 so + */ +tasks.register('downloadX86', Download) { + src BASE_PATH + "libs/x86/libcronet." + CronetVersion + ".so" + dest soPath + "/x86.so" + overwrite true + onlyIfModified true +} + +/** + * 更新Cronet版本时执行这个task + * 先更改gradle.properties 里面的版本号,然后再执行 + * gradlew app:downloadCronet + */ +tasks.register('downloadCronet') { + dependsOn downloadJar, downloadARM64, downloadARMv7, downloadX86_64, downloadX86 + + doLast { + StringBuilder sb = new StringBuilder("{") + def files = new File(soPath).listFiles() + for (File file : files) { + println file.name.replace(".so", "") + sb.append("\"").append(file.name.replace(".so", "")).append("\":\"").append(generateMD5(file)).append("\",") + } + sb.append("\"version\":\"").append(CronetVersion).append("\"}") + + println sb.toString() + + println assetsDir + def f1 = new File(assetsDir + "/cronet.json") + if (!f1.exists()) { + f1.parentFile.mkdirs() + f1.createNewFile() + } + f1.text = sb.toString() + + } + + +} + + diff --git a/app/google-services.json b/app/google-services.json new file mode 100755 index 000000000..fcbc11f0d --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,151 @@ +{ + "project_info": { + "project_number": "453392274790", + "firebase_url": "https://legado-fca69.firebaseio.com", + "project_id": "legado-fca69", + "storage_bucket": "legado-fca69.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7", + "android_client_info": { + "package_name": "io.legado.app.debug" + } + }, + "oauth_client": [ + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:453392274790:android:c1481c1c3d3f51eff624a7", + "android_client_info": { + "package_name": "io.legado.app.release" + } + }, + "oauth_client": [ + { + "client_id": "453392274790-trrgennt5njr1lhil1sgtf0ogcgd38fo.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.legado.app.release", + "certificate_hash": "fd67dba87b7b761631266f19ddde249054aac5c1" + } + }, + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7", + "android_client_info": { + "package_name": "io.legado.play.release" + } + }, + "oauth_client": [ + { + "client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.legado.play.release", + "certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b" + } + }, + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:453392274790:android:b891abd2331577dff624a7", + "android_client_info": { + "package_name": "io.legado.play.debug" + } + }, + "oauth_client": [ + { + "client_id": "453392274790-f8sjn6ohs72rg1dvp0pdvk42nkq54p0k.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "io.legado.play.debug", + "certificate_hash": "00819ace9891386e535967cbafd6a88f3797bd5b" + } + }, + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 000000000..845704b2a --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,142 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +# 混合时不使用大小写混合,混合后的类名为小写 +-dontusemixedcaseclassnames + +# 这句话能够使我们的项目混淆后产生映射文件 +# 包含有类名->混淆后类名的映射关系 +-verbose + +# 保留Annotation不混淆 +-keepattributes *Annotation*,InnerClasses + +# 避免混淆泛型 +-keepattributes Signature + +# 指定混淆是采用的算法,后面的参数是一个过滤器 +# 这个过滤器是谷歌推荐的算法,一般不做更改 +-optimizations !code/simplification/cast,!field/*,!class/merging/* + +-flattenpackagehierarchy + +############################################# +# +# Android开发中一些需要保留的公共部分 +# +############################################# +# 屏蔽错误Unresolved class name +#noinspection ShrinkerUnresolvedReference + +# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用 +# 记得proguard-android.txt中一定不要加-dontoptimize才起作用 +# 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制 +-assumenosideeffects class android.util.Log { + public static int v(...); + public static int i(...); + public static int w(...); + public static int d(...); + public static int e(...); +} + +# 保持js引擎调用的java类 +-keep class * extends io.legado.app.help.JsExtensions{*;} +# 数据类 +-keep class **.data.entities.**{*;} +# hutool-core hutool-crypto +-keep class +!cn.hutool.core.util.RuntimeUtil, +!cn.hutool.core.util.ClassLoaderUtil, +!cn.hutool.core.util.ReflectUtil, +!cn.hutool.core.util.SerializeUtil, +!cn.hutool.core.util.ClassUtil, +cn.hutool.core.codec.**, +cn.hutool.core.util.**{*;} +-keep class cn.hutool.crypto.**{*;} +-dontwarn cn.hutool.** +# 缓存 Cookie +-keep class **.help.http.CookieStore{*;} +-keep class **.help.CacheManager{*;} +# StrResponse +-keep class **.help.http.StrResponse{*;} + +# markwon +-dontwarn org.commonmark.ext.gfm.** + +-keep class okhttp3.*{*;} +-keep class okio.*{*;} +-keep class com.jayway.jsonpath.*{*;} + +# LiveEventBus +-keepclassmembers class androidx.lifecycle.LiveData { + *** mObservers; + *** mActiveCount; +} +-keepclassmembers class androidx.arch.core.internal.SafeIterableMap { + *** size(); + *** putIfAbsent(...); +} + +## ChangeBookSourceDialog initNavigationView +-keepclassmembers class androidx.appcompat.widget.Toolbar { + *** mNavButtonView; +} + +# MenuExtensions applyOpenTint +-keepnames class androidx.appcompat.view.menu.SubMenuBuilder +-keep class androidx.appcompat.view.menu.MenuBuilder { + *** setOptionalIconsVisible(...); + *** getNonActionItems(); +} + +# FileDocExtensions.kt treeDocumentFileConstructor +-keep class androidx.documentfile.provider.TreeDocumentFile { + (...); +} + +# JsoupXpath +-keep,allowobfuscation class * implements org.seimicrawler.xpath.core.AxisSelector{*;} +-keep,allowobfuscation class * implements org.seimicrawler.xpath.core.NodeTest{*;} +-keep,allowobfuscation class * implements org.seimicrawler.xpath.core.Function{*;} + +## JSOUP +-keep class org.jsoup.**{*;} +-dontwarn org.jspecify.annotations.NullMarked + +## ExoPlayer 反射设置ua 保证该私有变量不被混淆 +-keepclassmembers class androidx.media3.datasource.cache.CacheDataSource$Factory { + *** upstreamDataSourceFactory; +} +## ExoPlayer 如果还不能播放就取消注释这个 +# -keep class com.google.android.exoplayer2.** {*;} + +## 对外提供api +-keep class io.legado.app.api.ReturnData{*;} + +# Cronet +-keepclassmembers class org.chromium.net.X509Util { + *** sDefaultTrustManager; + *** sTestTrustManager; +} + +# Throwable +-keepnames class * extends java.lang.Throwable +-keepclassmembernames,allowobfuscation class * extends java.lang.Throwable{*;} diff --git a/app/schemas/io.legado.app.data.AppDatabase/1.json b/app/schemas/io.legado.app.data.AppDatabase/1.json new file mode 100755 index 000000000..1ab906cf0 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/1.json @@ -0,0 +1,1022 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "d9ed367fc7241a61e9f770d416c4f887", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleCategories` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleCategories", + "columnName": "ruleCategories", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `categories` TEXT, `read` INTEGER NOT NULL, `star` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "star", + "columnName": "star", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd9ed367fc7241a61e9f770d416c4f887')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/10.json b/app/schemas/io.legado.app.data.AppDatabase/10.json new file mode 100755 index 000000000..ddbecfb58 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/10.json @@ -0,0 +1,1176 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "a9744f575dad6d4774cccc433921973b", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`name`, `author`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "name", + "author" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a9744f575dad6d4774cccc433921973b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/11.json b/app/schemas/io.legado.app.data.AppDatabase/11.json new file mode 100755 index 000000000..660476fd8 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/11.json @@ -0,0 +1,1182 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "d3019908fa3212a7ac8eb87ac2f33369", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`name`, `author`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "name", + "author" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd3019908fa3212a7ac8eb87ac2f33369')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/12.json b/app/schemas/io.legado.app.data.AppDatabase/12.json new file mode 100755 index 000000000..1552fd7cb --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/12.json @@ -0,0 +1,1188 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "fa238e7524c215177f66110c847d327d", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`name`, `author`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "name", + "author" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa238e7524c215177f66110c847d327d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/13.json b/app/schemas/io.legado.app.data.AppDatabase/13.json new file mode 100755 index 000000000..3226a6f05 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/13.json @@ -0,0 +1,1194 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "da04f8cb7f257482f105b1274a7a351b", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`name`, `author`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "name", + "author" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'da04f8cb7f257482f105b1274a7a351b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/14.json b/app/schemas/io.legado.app.data.AppDatabase/14.json new file mode 100755 index 000000000..7f0f72ab8 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/14.json @@ -0,0 +1,1194 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "139ff0cc002ac7be67a60912cd26bac7", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '139ff0cc002ac7be67a60912cd26bac7')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/15.json b/app/schemas/io.legado.app.data.AppDatabase/15.json new file mode 100755 index 000000000..cb401ac94 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/15.json @@ -0,0 +1,1200 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "07a0976a08cbae60a16550af5663cde5", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '07a0976a08cbae60a16550af5663cde5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/16.json b/app/schemas/io.legado.app.data.AppDatabase/16.json new file mode 100755 index 000000000..80bb3925b --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/16.json @@ -0,0 +1,1226 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "ce9320370930dec28d85e2a77fad95e2", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))", + "fields": [ + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ce9320370930dec28d85e2a77fad95e2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/17.json b/app/schemas/io.legado.app.data.AppDatabase/17.json new file mode 100755 index 000000000..f061e584e --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/17.json @@ -0,0 +1,1226 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "ce9320370930dec28d85e2a77fad95e2", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))", + "fields": [ + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ce9320370930dec28d85e2a77fad95e2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/18.json b/app/schemas/io.legado.app.data.AppDatabase/18.json new file mode 100755 index 000000000..2ba39a806 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/18.json @@ -0,0 +1,1258 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "ec3badfc86fb8187260ab26fb78a2d3f", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))", + "fields": [ + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ec3badfc86fb8187260ab26fb78a2d3f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/19.json b/app/schemas/io.legado.app.data.AppDatabase/19.json new file mode 100755 index 000000000..bb5a0bb0f --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/19.json @@ -0,0 +1,1265 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "c58916ed4a4aece6092e21acf99845a1", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c58916ed4a4aece6092e21acf99845a1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/2.json b/app/schemas/io.legado.app.data.AppDatabase/2.json new file mode 100755 index 000000000..7fa0d4da9 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/2.json @@ -0,0 +1,1028 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "8164f697c57c68c7b82ec8e427214a88", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `star` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "star", + "columnName": "star", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8164f697c57c68c7b82ec8e427214a88')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/20.json b/app/schemas/io.legado.app.data.AppDatabase/20.json new file mode 100755 index 000000000..31058f1df --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/20.json @@ -0,0 +1,1271 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "2c443ea987b87d8daf2a6161a98d2d5c", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2c443ea987b87d8daf2a6161a98d2d5c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/21.json b/app/schemas/io.legado.app.data.AppDatabase/21.json new file mode 100755 index 000000000..80bc0b319 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/21.json @@ -0,0 +1,1283 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "eac4e5b7fdad840e82c1f607a3a8a46a", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, `config` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eac4e5b7fdad840e82c1f607a3a8a46a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/22.json b/app/schemas/io.legado.app.data.AppDatabase/22.json new file mode 100755 index 000000000..02a64213a --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/22.json @@ -0,0 +1,1277 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "9cf4f754700355578a8b8bf045c0e8e1", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9cf4f754700355578a8b8bf045c0e8e1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/23.json b/app/schemas/io.legado.app.data.AppDatabase/23.json new file mode 100755 index 000000000..48934cae4 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/23.json @@ -0,0 +1,1283 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "874aa30f88921306741b488dbad38536", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '874aa30f88921306741b488dbad38536')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/24.json b/app/schemas/io.legado.app.data.AppDatabase/24.json new file mode 100755 index 000000000..879ffdd5f --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/24.json @@ -0,0 +1,1324 @@ +{ + "formatVersion": 1, + "database": { + "version": 24, + "identityHash": "55416d5a8a8530659ae3e7f948c0058b", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '55416d5a8a8530659ae3e7f948c0058b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/25.json b/app/schemas/io.legado.app.data.AppDatabase/25.json new file mode 100755 index 000000000..c5e40e258 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/25.json @@ -0,0 +1,1368 @@ +{ + "formatVersion": 1, + "database": { + "version": 25, + "identityHash": "469ee9861faf7f562d7c60bc15a4a58b", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "sourceSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '469ee9861faf7f562d7c60bc15a4a58b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/26.json b/app/schemas/io.legado.app.data.AppDatabase/26.json new file mode 100755 index 000000000..573b83e23 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/26.json @@ -0,0 +1,1392 @@ +{ + "formatVersion": 1, + "database": { + "version": 26, + "identityHash": "e20aa63032efb23c9e9c269afd64e7d7", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e20aa63032efb23c9e9c269afd64e7d7')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/27.json b/app/schemas/io.legado.app.data.AppDatabase/27.json new file mode 100755 index 000000000..98e0181e7 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/27.json @@ -0,0 +1,1392 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "e20aa63032efb23c9e9c269afd64e7d7", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e20aa63032efb23c9e9c269afd64e7d7')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/28.json b/app/schemas/io.legado.app.data.AppDatabase/28.json new file mode 100755 index 000000000..180fb4fa4 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/28.json @@ -0,0 +1,1404 @@ +{ + "formatVersion": 1, + "database": { + "version": 28, + "identityHash": "f77119a40a8930665af834d03c8c5d25", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f77119a40a8930665af834d03c8c5d25')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/29.json b/app/schemas/io.legado.app.data.AppDatabase/29.json new file mode 100755 index 000000000..f31bbc341 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/29.json @@ -0,0 +1,1410 @@ +{ + "formatVersion": 1, + "database": { + "version": 29, + "identityHash": "85f1e7146f650af82aac6f9137eff815", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))", + "fields": [ + { + "fieldPath": "androidId", + "columnName": "androidId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "androidId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '85f1e7146f650af82aac6f9137eff815')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/3.json b/app/schemas/io.legado.app.data.AppDatabase/3.json new file mode 100755 index 000000000..0880d84a9 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/3.json @@ -0,0 +1,1036 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "a3ccd8882307290a450c49e09a4435f6", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `star` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "star", + "columnName": "star", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a3ccd8882307290a450c49e09a4435f6')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/30.json b/app/schemas/io.legado.app.data.AppDatabase/30.json new file mode 100755 index 000000000..e5bf2b031 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/30.json @@ -0,0 +1,1485 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "d9c8ef97ef4ffe0c1dbd57ca74bc4de4", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "epubChapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT, PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "href", + "columnName": "href", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentHref", + "columnName": "parentHref", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl", + "href" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_epubChapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_epubChapters_bookUrl_href", + "unique": true, + "columnNames": [ + "bookUrl", + "href" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `${TABLE_NAME}` (`bookUrl`, `href`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd9c8ef97ef4ffe0c1dbd57ca74bc4de4')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/31.json b/app/schemas/io.legado.app.data.AppDatabase/31.json new file mode 100755 index 000000000..fe8a10f4d --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/31.json @@ -0,0 +1,1422 @@ +{ + "formatVersion": 1, + "database": { + "version": 31, + "identityHash": "d1c390e708a1e89c7d016cdd2e0b2e88", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd1c390e708a1e89c7d016cdd2e0b2e88')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/32.json b/app/schemas/io.legado.app.data.AppDatabase/32.json new file mode 100755 index 000000000..03fd2016f --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/32.json @@ -0,0 +1,1422 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "d1c390e708a1e89c7d016cdd2e0b2e88", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd1c390e708a1e89c7d016cdd2e0b2e88')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/33.json b/app/schemas/io.legado.app.data.AppDatabase/33.json new file mode 100755 index 000000000..f4deedbd2 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/33.json @@ -0,0 +1,1417 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "6dad1518f359667b4d740fc6a1f44a21", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6dad1518f359667b4d740fc6a1f44a21')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/34.json b/app/schemas/io.legado.app.data.AppDatabase/34.json new file mode 100755 index 000000000..3fe40c410 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/34.json @@ -0,0 +1,1423 @@ +{ + "formatVersion": 1, + "database": { + "version": 34, + "identityHash": "2e519f1f67ca16091cbc3891c1b71c66", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2e519f1f67ca16091cbc3891c1b71c66')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/35.json b/app/schemas/io.legado.app.data.AppDatabase/35.json new file mode 100755 index 000000000..9e3eeb6fa --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/35.json @@ -0,0 +1,1441 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "25948a8defe4d091514bb725b4db7683", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `concurrentRate` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '25948a8defe4d091514bb725b4db7683')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/36.json b/app/schemas/io.legado.app.data.AppDatabase/36.json new file mode 100755 index 000000000..d99c1b8fb --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/36.json @@ -0,0 +1,1441 @@ +{ + "formatVersion": 1, + "database": { + "version": 36, + "identityHash": "25948a8defe4d091514bb725b4db7683", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `concurrentRate` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '25948a8defe4d091514bb725b4db7683')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/37.json b/app/schemas/io.legado.app.data.AppDatabase/37.json new file mode 100755 index 000000000..c5c21ec88 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/37.json @@ -0,0 +1,1459 @@ +{ + "formatVersion": 1, + "database": { + "version": 37, + "identityHash": "11ebd6a72eb3f9ccd6ca46bc5535bca5", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `concurrentRate` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11ebd6a72eb3f9ccd6ca46bc5535bca5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/38.json b/app/schemas/io.legado.app.data.AppDatabase/38.json new file mode 100755 index 000000000..ab7415219 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/38.json @@ -0,0 +1,1465 @@ +{ + "formatVersion": 1, + "database": { + "version": 38, + "identityHash": "5211699415b40f58b06d4136d14173d1", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `concurrentRate` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5211699415b40f58b06d4136d14173d1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/39.json b/app/schemas/io.legado.app.data.AppDatabase/39.json new file mode 100755 index 000000000..290db5cb0 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/39.json @@ -0,0 +1,1471 @@ +{ + "formatVersion": 1, + "database": { + "version": 39, + "identityHash": "8cfa1fb6bb9a65c04bfe563680126a4f", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8cfa1fb6bb9a65c04bfe563680126a4f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/4.json b/app/schemas/io.legado.app.data.AppDatabase/4.json new file mode 100755 index 000000000..aadc551fb --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/4.json @@ -0,0 +1,1093 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "3b81b2e900be34b8ceb48aaacc6b1004", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3b81b2e900be34b8ceb48aaacc6b1004')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/40.json b/app/schemas/io.legado.app.data.AppDatabase/40.json new file mode 100755 index 000000000..0bbd935aa --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/40.json @@ -0,0 +1,1483 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "09617e0520cd8ec1671d812a866b45a4", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09617e0520cd8ec1671d812a866b45a4')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/41.json b/app/schemas/io.legado.app.data.AppDatabase/41.json new file mode 100755 index 000000000..c04ce26a1 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/41.json @@ -0,0 +1,1513 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "6fbd1d1b3918bcc6db113ad108e6b924", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6fbd1d1b3918bcc6db113ad108e6b924')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/42.json b/app/schemas/io.legado.app.data.AppDatabase/42.json new file mode 100755 index 000000000..b14284f09 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/42.json @@ -0,0 +1,1519 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "5bef05ac6abeaa4b82c3ff6e9e6bd7b3", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5bef05ac6abeaa4b82c3ff6e9e6bd7b3')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/43.json b/app/schemas/io.legado.app.data.AppDatabase/43.json new file mode 100755 index 000000000..f375a4729 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/43.json @@ -0,0 +1,1537 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "b97eb5297faaacb44c2274233f52d250", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b97eb5297faaacb44c2274233f52d250')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/44.json b/app/schemas/io.legado.app.data.AppDatabase/44.json new file mode 100755 index 000000000..7062c8af6 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/44.json @@ -0,0 +1,1557 @@ +{ + "formatVersion": 1, + "database": { + "version": 44, + "identityHash": "fdfaa67979c13dd76db0a1b594bc5904", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fdfaa67979c13dd76db0a1b594bc5904')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/45.json b/app/schemas/io.legado.app.data.AppDatabase/45.json new file mode 100755 index 000000000..6c760036e --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/45.json @@ -0,0 +1,1618 @@ +{ + "formatVersion": 1, + "database": { + "version": 45, + "identityHash": "63272539e04e405abfd79e27ea55db75", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '63272539e04e405abfd79e27ea55db75')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/46.json b/app/schemas/io.legado.app.data.AppDatabase/46.json new file mode 100755 index 000000000..56b556d67 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/46.json @@ -0,0 +1,1618 @@ +{ + "formatVersion": 1, + "database": { + "version": 46, + "identityHash": "63272539e04e405abfd79e27ea55db75", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '63272539e04e405abfd79e27ea55db75')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/47.json b/app/schemas/io.legado.app.data.AppDatabase/47.json new file mode 100755 index 000000000..bb6f79b2e --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/47.json @@ -0,0 +1,1625 @@ +{ + "formatVersion": 1, + "database": { + "version": 47, + "identityHash": "985b7a8fbf3f49e6eb444e4f4472bdee", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '985b7a8fbf3f49e6eb444e4f4472bdee')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/48.json b/app/schemas/io.legado.app.data.AppDatabase/48.json new file mode 100755 index 000000000..cebc5ae18 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/48.json @@ -0,0 +1,1633 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "6348b1307bb9fb4f5482fb94a723338a", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6348b1307bb9fb4f5482fb94a723338a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/49.json b/app/schemas/io.legado.app.data.AppDatabase/49.json new file mode 100755 index 000000000..60bf90828 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/49.json @@ -0,0 +1,1654 @@ +{ + "formatVersion": 1, + "database": { + "version": 49, + "identityHash": "2a6f5ee3d0ed9ac13f15183a04a4af45", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2a6f5ee3d0ed9ac13f15183a04a4af45')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/5.json b/app/schemas/io.legado.app.data.AppDatabase/5.json new file mode 100755 index 000000000..840c85b66 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/5.json @@ -0,0 +1,1093 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "a355f8e02ebe0b13464573b1420a7b90", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a355f8e02ebe0b13464573b1420a7b90')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/50.json b/app/schemas/io.legado.app.data.AppDatabase/50.json new file mode 100755 index 000000000..05ebf3e6f --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/50.json @@ -0,0 +1,1666 @@ +{ + "formatVersion": 1, + "database": { + "version": 50, + "identityHash": "524cf3564400bd897a49355afd984ac1", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '524cf3564400bd897a49355afd984ac1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/51.json b/app/schemas/io.legado.app.data.AppDatabase/51.json new file mode 100755 index 000000000..e11ecf0e3 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/51.json @@ -0,0 +1,1674 @@ +{ + "formatVersion": 1, + "database": { + "version": 51, + "identityHash": "028c189be828501449b61b32ab47a36f", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '028c189be828501449b61b32ab47a36f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/52.json b/app/schemas/io.legado.app.data.AppDatabase/52.json new file mode 100755 index 000000000..4f3ee4e9a --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/52.json @@ -0,0 +1,1680 @@ +{ + "formatVersion": 1, + "database": { + "version": 52, + "identityHash": "0221c385ed7393f47afe9579d3106541", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0221c385ed7393f47afe9579d3106541')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/53.json b/app/schemas/io.legado.app.data.AppDatabase/53.json new file mode 100755 index 000000000..518a1bfb7 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/53.json @@ -0,0 +1,1692 @@ +{ + "formatVersion": 1, + "database": { + "version": 53, + "identityHash": "c55dac79aa9cc3dcdb72e91da4282005", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c55dac79aa9cc3dcdb72e91da4282005')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/54.json b/app/schemas/io.legado.app.data.AppDatabase/54.json new file mode 100755 index 000000000..897fb0f7e --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/54.json @@ -0,0 +1,1711 @@ +{ + "formatVersion": 1, + "database": { + "version": 54, + "identityHash": "2b32177325d903e84445cc80ad7cbce8", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT '', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 1, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2b32177325d903e84445cc80ad7cbce8')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/55.json b/app/schemas/io.legado.app.data.AppDatabase/55.json new file mode 100755 index 000000000..c4d25304c --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/55.json @@ -0,0 +1,1711 @@ +{ + "formatVersion": 1, + "database": { + "version": 55, + "identityHash": "7dc698b0bf395df06befb13d41df87b9", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7dc698b0bf395df06befb13d41df87b9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/56.json b/app/schemas/io.legado.app.data.AppDatabase/56.json new file mode 100755 index 000000000..c4959f96f --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/56.json @@ -0,0 +1,1711 @@ +{ + "formatVersion": 1, + "database": { + "version": 56, + "identityHash": "7dc698b0bf395df06befb13d41df87b9", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7dc698b0bf395df06befb13d41df87b9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/57.json b/app/schemas/io.legado.app.data.AppDatabase/57.json new file mode 100755 index 000000000..9c61e1832 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/57.json @@ -0,0 +1,1717 @@ +{ + "formatVersion": 1, + "database": { + "version": 57, + "identityHash": "ae968e35603fe39a0141d0983d993de1", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ae968e35603fe39a0141d0983d993de1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/58.json b/app/schemas/io.legado.app.data.AppDatabase/58.json new file mode 100755 index 000000000..e4e6bdcf4 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/58.json @@ -0,0 +1,1724 @@ +{ + "formatVersion": 1, + "database": { + "version": 58, + "identityHash": "dce5917572677d2b19bbc43809efb40c", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "deviceId", + "bookName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "type", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dce5917572677d2b19bbc43809efb40c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/59.json b/app/schemas/io.legado.app.data.AppDatabase/59.json new file mode 100755 index 000000000..189a372bc --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/59.json @@ -0,0 +1,1740 @@ +{ + "formatVersion": 1, + "database": { + "version": 59, + "identityHash": "ddae0fd3a6b7ec874a028754a8cdd528", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ddae0fd3a6b7ec874a028754a8cdd528')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/6.json b/app/schemas/io.legado.app.data.AppDatabase/6.json new file mode 100755 index 000000000..2e61f6107 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/6.json @@ -0,0 +1,1131 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "af70ea583587e17c968d29f41bb3c0d6", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'af70ea583587e17c968d29f41bb3c0d6')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/60.json b/app/schemas/io.legado.app.data.AppDatabase/60.json new file mode 100755 index 000000000..0c0cab95f --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/60.json @@ -0,0 +1,1778 @@ +{ + "formatVersion": 1, + "database": { + "version": 60, + "identityHash": "19e1f66252eb41afc0990f202fe4527a", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '19e1f66252eb41afc0990f202fe4527a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/61.json b/app/schemas/io.legado.app.data.AppDatabase/61.json new file mode 100755 index 000000000..cbf656053 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/61.json @@ -0,0 +1,1786 @@ +{ + "formatVersion": 1, + "database": { + "version": 61, + "identityHash": "9e424026dc2c4dba847b0d9a5f3f788e", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9e424026dc2c4dba847b0d9a5f3f788e')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/62.json b/app/schemas/io.legado.app.data.AppDatabase/62.json new file mode 100755 index 000000000..4982bca81 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/62.json @@ -0,0 +1,1830 @@ +{ + "formatVersion": 1, + "database": { + "version": 62, + "identityHash": "965c5f1d4c60d25b4b46efb17c575c85", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `show` INTEGER NOT NULL, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '965c5f1d4c60d25b4b46efb17c575c85')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/63.json b/app/schemas/io.legado.app.data.AppDatabase/63.json new file mode 100755 index 000000000..af2d68a4a --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/63.json @@ -0,0 +1,1838 @@ +{ + "formatVersion": 1, + "database": { + "version": 63, + "identityHash": "5763792a4d08dc4cd7ebec9fb3215458", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5763792a4d08dc4cd7ebec9fb3215458')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/64.json b/app/schemas/io.legado.app.data.AppDatabase/64.json new file mode 100755 index 000000000..c2c729758 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/64.json @@ -0,0 +1,1847 @@ +{ + "formatVersion": 1, + "database": { + "version": 64, + "identityHash": "c9b9979578e5ae84c1b424b1aa590efa", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledReview", + "columnName": "enabledReview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c9b9979578e5ae84c1b424b1aa590efa')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/65.json b/app/schemas/io.legado.app.data.AppDatabase/65.json new file mode 100755 index 000000000..6a2126ad6 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/65.json @@ -0,0 +1,1853 @@ +{ + "formatVersion": 1, + "database": { + "version": 65, + "identityHash": "ccfe4ad59c8caef014b1c754b69e6d05", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ccfe4ad59c8caef014b1c754b69e6d05')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/66.json b/app/schemas/io.legado.app.data.AppDatabase/66.json new file mode 100755 index 000000000..ea7d6e9dc --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/66.json @@ -0,0 +1,1859 @@ +{ + "formatVersion": 1, + "database": { + "version": 66, + "identityHash": "ef55cf538ab8076ccaf821cbc7f6582f", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ef55cf538ab8076ccaf821cbc7f6582f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/67.json b/app/schemas/io.legado.app.data.AppDatabase/67.json new file mode 100755 index 000000000..5da3ea902 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/67.json @@ -0,0 +1,1879 @@ +{ + "formatVersion": 1, + "database": { + "version": 67, + "identityHash": "48855e5272b008e4c6b202a41654b575", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '48855e5272b008e4c6b202a41654b575')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/68.json b/app/schemas/io.legado.app.data.AppDatabase/68.json new file mode 100755 index 000000000..b3c1d9a86 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/68.json @@ -0,0 +1,1885 @@ +{ + "formatVersion": 1, + "database": { + "version": 68, + "identityHash": "9b6070f339f55e90e4cceaeb89b4a698", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9b6070f339f55e90e4cceaeb89b4a698')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/69.json b/app/schemas/io.legado.app.data.AppDatabase/69.json new file mode 100755 index 000000000..2e0deef65 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/69.json @@ -0,0 +1,1885 @@ +{ + "formatVersion": 1, + "database": { + "version": 69, + "identityHash": "9b6070f339f55e90e4cceaeb89b4a698", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9b6070f339f55e90e4cceaeb89b4a698')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/7.json b/app/schemas/io.legado.app.data.AppDatabase/7.json new file mode 100755 index 000000000..3a1fb4165 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/7.json @@ -0,0 +1,1131 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "af70ea583587e17c968d29f41bb3c0d6", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'af70ea583587e17c968d29f41bb3c0d6')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/70.json b/app/schemas/io.legado.app.data.AppDatabase/70.json new file mode 100755 index 000000000..515bc2a2a --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/70.json @@ -0,0 +1,1891 @@ +{ + "formatVersion": 1, + "database": { + "version": 70, + "identityHash": "ab3b8675fe5c3152003e20a73f022b0c", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldOverrideUrlLoading", + "columnName": "shouldOverrideUrlLoading", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ab3b8675fe5c3152003e20a73f022b0c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/71.json b/app/schemas/io.legado.app.data.AppDatabase/71.json new file mode 100755 index 000000000..48c7d4139 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/71.json @@ -0,0 +1,1898 @@ +{ + "formatVersion": 1, + "database": { + "version": 71, + "identityHash": "6e17b5366286b868f2912c12d6d8e467", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, `syncTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncTime", + "columnName": "syncTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldOverrideUrlLoading", + "columnName": "shouldOverrideUrlLoading", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6e17b5366286b868f2912c12d6d8e467')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/72.json b/app/schemas/io.legado.app.data.AppDatabase/72.json new file mode 100755 index 000000000..03a2072bd --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/72.json @@ -0,0 +1,1912 @@ +{ + "formatVersion": 1, + "database": { + "version": 72, + "identityHash": "53d960f1fdc04a6f157a25b269a0d93b", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, `syncTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncTime", + "columnName": "syncTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldOverrideUrlLoading", + "columnName": "shouldOverrideUrlLoading", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '53d960f1fdc04a6f157a25b269a0d93b')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/73.json b/app/schemas/io.legado.app.data.AppDatabase/73.json new file mode 100755 index 000000000..0fc62d453 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/73.json @@ -0,0 +1,1918 @@ +{ + "formatVersion": 1, + "database": { + "version": 73, + "identityHash": "63617164079ebbdaf82176384c39bba5", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, `syncTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncTime", + "columnName": "syncTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `wordCount` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldOverrideUrlLoading", + "columnName": "shouldOverrideUrlLoading", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '63617164079ebbdaf82176384c39bba5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/74.json b/app/schemas/io.legado.app.data.AppDatabase/74.json new file mode 100755 index 000000000..91913bcc7 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/74.json @@ -0,0 +1,1930 @@ +{ + "formatVersion": 1, + "database": { + "version": 74, + "identityHash": "c492bca5d9c4b4b34a0808d8cce9e7ba", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, `syncTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncTime", + "columnName": "syncTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `wordCount` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldOverrideUrlLoading", + "columnName": "shouldOverrideUrlLoading", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `title` TEXT, `readTime` INTEGER, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c492bca5d9c4b4b34a0808d8cce9e7ba')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/75.json b/app/schemas/io.legado.app.data.AppDatabase/75.json new file mode 100755 index 000000000..0c7dd7e39 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/75.json @@ -0,0 +1,1935 @@ +{ + "formatVersion": 1, + "database": { + "version": 75, + "identityHash": "e22b36ff0280a224a30d08dd6e9bd7be", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, `syncTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'loc_book'" + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readConfig", + "columnName": "readConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncTime", + "columnName": "syncTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_books_name_author", + "unique": true, + "columnNames": [ + "name", + "author" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cover", + "columnName": "cover", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableRefresh", + "columnName": "enableRefresh", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "show", + "columnName": "show", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "bookSort", + "columnName": "bookSort", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "groupId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceComment", + "columnName": "bookSourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "exploreScreen", + "columnName": "exploreScreen", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleReview", + "columnName": "ruleReview", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookSourceUrl" + ] + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `wordCount` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isVolume", + "columnName": "isVolume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isVip", + "columnName": "isVip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPay", + "columnName": "isPay", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startFragmentId", + "columnName": "startFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endFragmentId", + "columnName": "endFragmentId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url", + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scopeTitle", + "columnName": "scopeTitle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "scopeContent", + "columnName": "scopeContent", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "excludeScope", + "columnName": "excludeScope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "timeoutMillisecond", + "columnName": "timeoutMillisecond", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3000" + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterWordCountText", + "columnName": "chapterWordCountText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "chapterWordCount", + "columnName": "chapterWordCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "respondTime", + "columnName": "respondTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bookUrl" + ] + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "word" + ] + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "url" + ] + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceComment", + "columnName": "sourceComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variableComment", + "columnName": "variableComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverDecodeJs", + "columnName": "coverDecodeJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "singleUrl", + "columnName": "singleUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "articleStyle", + "columnName": "articleStyle", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWhitelist", + "columnName": "contentWhitelist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentBlacklist", + "columnName": "contentBlacklist", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shouldOverrideUrlLoading", + "columnName": "shouldOverrideUrlLoading", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "style", + "columnName": "style", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "injectJs", + "columnName": "injectJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceUrl" + ] + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookAuthor", + "columnName": "bookAuthor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterPos", + "columnName": "chapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookText", + "columnName": "bookText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "time" + ] + }, + "indices": [ + { + "name": "index_bookmarks_bookName_bookAuthor", + "unique": false, + "columnNames": [ + "bookName", + "bookAuthor" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `read` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `title` TEXT, `readTime` INTEGER, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "record" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `variable` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'默认分组'" + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "origin", + "link" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "example", + "columnName": "example", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "readRecord", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readTime", + "columnName": "readTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastRead", + "columnName": "lastRead", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "deviceId", + "bookName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "httpTTS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "concurrentRate", + "columnName": "concurrentRate", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUi", + "columnName": "loginUi", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "jsLib", + "columnName": "jsLib", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabledCookieJar", + "columnName": "enabledCookieJar", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "loginCheckJs", + "columnName": "loginCheckJs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deadline", + "columnName": "deadline", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_caches_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "ruleSubs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoUpdate", + "columnName": "autoUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dictRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urlRule", + "columnName": "urlRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showRule", + "columnName": "showRule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "keyboardAssists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "serialNo", + "columnName": "serialNo", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "type", + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sortNumber", + "columnName": "sortNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [ + { + "viewName": "book_sources_part", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore, \n (loginUrl is not null and trim(loginUrl) <> '') hasLoginUrl, lastUpdateTime, respondTime, weight, \n (exploreUrl is not null and trim(exploreUrl) <> '') hasExploreUrl \n from book_sources" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e22b36ff0280a224a30d08dd6e9bd7be')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/8.json b/app/schemas/io.legado.app.data.AppDatabase/8.json new file mode 100755 index 000000000..dcd9838d7 --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/8.json @@ -0,0 +1,1157 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "0c09d0b5970a01069c4381648f793da7", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c09d0b5970a01069c4381648f793da7')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/io.legado.app.data.AppDatabase/9.json b/app/schemas/io.legado.app.data.AppDatabase/9.json new file mode 100755 index 000000000..a298e14ab --- /dev/null +++ b/app/schemas/io.legado.app.data.AppDatabase/9.json @@ -0,0 +1,1164 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "8da976febbd44e9e028b951b42583f9a", + "entities": [ + { + "tableName": "books", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`name`, `author`))", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customTag", + "columnName": "customTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customCoverUrl", + "columnName": "customCoverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customIntro", + "columnName": "customIntro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "charset", + "columnName": "charset", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTime", + "columnName": "latestChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckTime", + "columnName": "lastCheckTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckCount", + "columnName": "lastCheckCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalChapterNum", + "columnName": "totalChapterNum", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTitle", + "columnName": "durChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durChapterIndex", + "columnName": "durChapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterPos", + "columnName": "durChapterPos", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durChapterTime", + "columnName": "durChapterTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "canUpdate", + "columnName": "canUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "useReplaceRule", + "columnName": "useReplaceRule", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "name", + "author" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_books_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "book_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`groupId`))", + "fields": [ + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupName", + "columnName": "groupName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "groupId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "book_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceUrl` TEXT NOT NULL, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `header` TEXT, `loginUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, PRIMARY KEY(`bookSourceUrl`))", + "fields": [ + { + "fieldPath": "bookSourceName", + "columnName": "bookSourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceGroup", + "columnName": "bookSourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bookSourceUrl", + "columnName": "bookSourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookSourceType", + "columnName": "bookSourceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrlPattern", + "columnName": "bookUrlPattern", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabledExplore", + "columnName": "enabledExplore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loginUrl", + "columnName": "loginUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastUpdateTime", + "columnName": "lastUpdateTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exploreUrl", + "columnName": "exploreUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleExplore", + "columnName": "ruleExplore", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "searchUrl", + "columnName": "searchUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleSearch", + "columnName": "ruleSearch", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleBookInfo", + "columnName": "ruleBookInfo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleToc", + "columnName": "ruleToc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "bookSourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_book_sources_bookSourceUrl", + "unique": false, + "columnNames": [ + "bookSourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "chapters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "resourceUrl", + "columnName": "resourceUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "url", + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_chapters_bookUrl", + "unique": false, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_chapters_bookUrl_index", + "unique": true, + "columnNames": [ + "bookUrl", + "index" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)" + } + ], + "foreignKeys": [ + { + "table": "books", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "bookUrl" + ], + "referencedColumns": [ + "bookUrl" + ] + } + ] + }, + { + "tableName": "replace_rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `group` TEXT, `pattern` TEXT NOT NULL, `replacement` TEXT NOT NULL, `scope` TEXT, `isEnabled` INTEGER NOT NULL, `isRegex` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pattern", + "columnName": "pattern", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "replacement", + "columnName": "replacement", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEnabled", + "columnName": "isEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRegex", + "columnName": "isRegex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "sortOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_replace_rules_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "searchBooks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originName", + "columnName": "originName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "coverUrl", + "columnName": "coverUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "intro", + "columnName": "intro", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wordCount", + "columnName": "wordCount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latestChapterTitle", + "columnName": "latestChapterTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tocUrl", + "columnName": "tocUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "variable", + "columnName": "variable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "originOrder", + "columnName": "originOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "bookUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_searchBooks_bookUrl", + "unique": true, + "columnNames": [ + "bookUrl" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)" + }, + { + "name": "index_searchBooks_origin", + "unique": false, + "columnNames": [ + "origin" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)" + } + ], + "foreignKeys": [ + { + "table": "book_sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "origin" + ], + "referencedColumns": [ + "bookSourceUrl" + ] + } + ] + }, + { + "tableName": "search_keywords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))", + "fields": [ + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usage", + "columnName": "usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUseTime", + "columnName": "lastUseTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "word" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_search_keywords_word", + "unique": true, + "columnNames": [ + "word" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))", + "fields": [ + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "url" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_cookies_url", + "unique": true, + "columnNames": [ + "url" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssSources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `enabled` INTEGER NOT NULL, `sortUrl` TEXT, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `header` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, PRIMARY KEY(`sourceUrl`))", + "fields": [ + { + "fieldPath": "sourceUrl", + "columnName": "sourceUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceName", + "columnName": "sourceName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceIcon", + "columnName": "sourceIcon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourceGroup", + "columnName": "sourceGroup", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortUrl", + "columnName": "sortUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleArticles", + "columnName": "ruleArticles", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleNextPage", + "columnName": "ruleNextPage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleTitle", + "columnName": "ruleTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rulePubDate", + "columnName": "rulePubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleDescription", + "columnName": "ruleDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleImage", + "columnName": "ruleImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleLink", + "columnName": "ruleLink", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ruleContent", + "columnName": "ruleContent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "header", + "columnName": "header", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enableJs", + "columnName": "enableJs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loadWithBaseUrl", + "columnName": "loadWithBaseUrl", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customOrder", + "columnName": "customOrder", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sourceUrl" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_rssSources_sourceUrl", + "unique": false, + "columnNames": [ + "sourceUrl" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `pageIndex` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))", + "fields": [ + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookUrl", + "columnName": "bookUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bookName", + "columnName": "bookName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "chapterIndex", + "columnName": "chapterIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageIndex", + "columnName": "pageIndex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chapterName", + "columnName": "chapterName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "time" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_bookmarks_time", + "unique": true, + "columnNames": [ + "time" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "rssArticles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `read` INTEGER NOT NULL, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssReadRecords", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))", + "fields": [ + { + "fieldPath": "record", + "columnName": "record", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "read", + "columnName": "read", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "record" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rssStars", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, PRIMARY KEY(`origin`, `link`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starTime", + "columnName": "starTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "link", + "columnName": "link", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubDate", + "columnName": "pubDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "origin", + "link" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "txtTocRules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `rule` TEXT NOT NULL, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rule", + "columnName": "rule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serialNumber", + "columnName": "serialNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8da976febbd44e9e028b951b42583f9a')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/legado/app/AndroidJsTest.kt b/app/src/androidTest/java/io/legado/app/AndroidJsTest.kt new file mode 100755 index 000000000..f2d236472 --- /dev/null +++ b/app/src/androidTest/java/io/legado/app/AndroidJsTest.kt @@ -0,0 +1,84 @@ +package io.legado.app + +import cn.hutool.core.lang.JarClassLoader +import com.script.ScriptBindings +import com.script.rhino.RhinoScriptEngine +import dalvik.system.DexClassLoader +import org.intellij.lang.annotations.Language +import org.junit.Assert +import org.junit.Test +import org.mozilla.javascript.DefiningClassLoader +import java.net.URLClassLoader + +class AndroidJsTest { + + @Test + fun testPackages() { + @Language("js") + val js = """ + var accessKeyId = '1111'; + var accessKeySecret = '2222'; + var timestamp = '3333'; + var aly = new JavaImporter(Packages.javax.crypto.Mac, Packages.javax.crypto.spec.SecretKeySpec, Packages.javax.xml.bind.DatatypeConverter, Packages.java.net.URLEncoder, Packages.java.lang.String, Packages.android.util.Base64); + with (aly) { + function percentEncode(value) { + return URLEncoder.encode(value, "UTF-8").replace("+", "%20") + .replace("*", "%2A").replace("%7E", "~") + } + + function sign(stringToSign, accessKeySecret) { + var mac = Mac.getInstance('HmacSHA1'); + mac.init(new SecretKeySpec(String(accessKeySecret + '&').getBytes("UTF-8"), "HmacSHA1")); + var signData = mac.doFinal(String(stringToSign).getBytes("UTF-8")); + var signBase64 = Base64.encodeToString(signData, Base64.NO_WRAP); + var signUrlEncode = percentEncode(signBase64); + return signUrlEncode; + } + } + var query = 'AccessKeyId=' + accessKeyId + '&Action=CreateToken&Format=JSON&RegionId=cn-shanghai&SignatureMethod=HMAC-SHA1&SignatureNonce=' + "xxccrr" + '&SignatureVersion=1.0&Timestamp=' + percentEncode(timestamp) + '&Version=2019-02-28'; + var signStr = sign('GET&' + percentEncode('/') + '&' + percentEncode(query), accessKeySecret); + var queryStringWithSign = "Signature=" + signStr + "&" + query; + queryStringWithSign + """.trimIndent() + RhinoScriptEngine.eval(js) + @Language("js") + val js1 = """ + var returnData = new Packages.io.legado.app.api.ReturnData() + returnData.getErrorMsg() + """.trimIndent() + val result1 = RhinoScriptEngine.eval(js1) + Assert.assertEquals(result1, "未知错误,请联系开发者!") + } + + @Test + fun testPackages1() { + URLClassLoader.getSystemClassLoader() + DefiningClassLoader.getSystemClassLoader() + JarClassLoader.getSystemClassLoader() + DexClassLoader.getSystemClassLoader() + @Language("js") + val js = """ + var ji = new JavaImporter(Packages.org.mozilla.javascript.DefiningClassLoader) + with(ji) { + let x = DefiningClassLoader.getSystemClassLoader() + } + """.trimIndent() + RhinoScriptEngine.eval(js) + } + + @Test + fun testMap() { + val map = hashMapOf("id" to "3242532321") + val bindings = ScriptBindings() + bindings["result"] = map + @Language("js") + val jsMap = "$=result;id=$.id;id" + val result = RhinoScriptEngine.eval(jsMap, bindings) + Assert.assertEquals("3242532321", result) + @Language("js") + val jsMap1 = """result.get("id")""" + val result1 = RhinoScriptEngine.eval(jsMap1, bindings) + Assert.assertEquals("3242532321", result1) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/legado/app/ExampleInstrumentedTest.kt b/app/src/androidTest/java/io/legado/app/ExampleInstrumentedTest.kt new file mode 100755 index 000000000..56130db62 --- /dev/null +++ b/app/src/androidTest/java/io/legado/app/ExampleInstrumentedTest.kt @@ -0,0 +1,34 @@ +package io.legado.app + +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun testContentProvider() { + // Context of the app under test. + val appContext = ApplicationProvider.getApplicationContext() + Log.d( + "test", + appContext.contentResolver.query( + Uri.parse("content://io.legado.app.api.ReaderProvider/sources/query"), + null, + null, + null, + null + ) + !!.getString(0) + ) + } +} diff --git a/app/src/androidTest/java/io/legado/app/HttpTest.kt b/app/src/androidTest/java/io/legado/app/HttpTest.kt new file mode 100755 index 000000000..dd5fb7965 --- /dev/null +++ b/app/src/androidTest/java/io/legado/app/HttpTest.kt @@ -0,0 +1,83 @@ +package io.legado.app + +import android.app.DownloadManager +import android.net.Uri +import android.os.Environment +import android.webkit.WebSettings +import android.webkit.WebView +import io.legado.app.help.config.AppConfig +import io.legado.app.utils.runOnUI +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.Test +import splitties.init.appCtx +import splitties.systemservices.downloadManager + +class HttpTest { + + @Test + fun test() { + webViewDownloadTest() + } + + private fun webViewDownloadTest() { + runOnUI { + val webView = WebView(appCtx) + val settings = webView.settings + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.blockNetworkImage = true + settings.userAgentString = AppConfig.userAgent + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + print(url) + webView.destroy() + } + webView.loadUrl("https://gj.legado.cc/legado/?url=https://miaogongzi.lanzout.com/iITmP0s7y26d&type=down") + } + } + + private fun downloadManagerTest() { + runBlocking { + val request = + DownloadManager.Request(Uri.parse("https://gj.legado.cc/legado/?url=https://miaogongzi.lanzout.com/iITmP0s7y26d&type=down")) + // 设置通知 + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) + // 设置下载文件保存的路径和文件名 + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "test.txt") + // 添加一个下载任务 + val downloadId = downloadManager.enqueue(request) + val query = DownloadManager.Query() + query.setFilterById(downloadId) + repeat(30) { + downloadManager.query(query).use { cursor -> + if (cursor.moveToFirst()) { + val progressIndex = + cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + val fileSizeIndex = + cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS) + val progress = cursor.getInt(progressIndex) + val max = cursor.getInt(fileSizeIndex) + val status = when (cursor.getInt(statusIndex)) { + DownloadManager.STATUS_PAUSED -> appCtx.getString(R.string.pause) + DownloadManager.STATUS_PENDING -> appCtx.getString(R.string.wait_download) + DownloadManager.STATUS_RUNNING -> appCtx.getString(R.string.downloading) + DownloadManager.STATUS_SUCCESSFUL -> { + appCtx.getString(R.string.download_success) + } + + DownloadManager.STATUS_FAILED -> appCtx.getString(R.string.download_error) + else -> appCtx.getString(R.string.unknown_state) + } + print(status) + delay(1000) + } else { + return@runBlocking + } + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/legado/app/HttpTtsTest.kt b/app/src/androidTest/java/io/legado/app/HttpTtsTest.kt new file mode 100755 index 000000000..e9b1601bb --- /dev/null +++ b/app/src/androidTest/java/io/legado/app/HttpTtsTest.kt @@ -0,0 +1,26 @@ +package io.legado.app + +import io.legado.app.help.config.AppConfig +import io.legado.app.model.analyzeRule.AnalyzeUrl +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class HttpTtsTest { + + @Test + fun test() { + val url = """ + http://tsn.baidu.com/text2audio,{ + "method": "POST", + "body": "tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{(speakSpeed + 5) / 10 + 4}}&per=4114&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=220&vol=5&aue=6&pit=5&_res_tag_=audio" + } + """.trimIndent() + val analyzeUrl = + AnalyzeUrl(url, speakText = "魔神", speakSpeed = AppConfig.speechRatePlay + 5) + runBlocking { + val response = analyzeUrl.getResponseAwait() + response.headers + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/legado/app/MigrationTest.kt b/app/src/androidTest/java/io/legado/app/MigrationTest.kt new file mode 100755 index 000000000..dbac4197a --- /dev/null +++ b/app/src/androidTest/java/io/legado/app/MigrationTest.kt @@ -0,0 +1,51 @@ +package io.legado.app + +import androidx.room.Room +import androidx.room.migration.Migration +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.legado.app.data.AppDatabase +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class MigrationTest { + + private val TEST_DB = "migration-test" + + private val ALL_MIGRATIONS = arrayOf( + + ) + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + @Throws(IOException::class) + fun migrateAll() { + // Create earliest version of the database. + helper.createDatabase(TEST_DB, 50).apply { + close() + } + + // Open latest version of the database. Room will validate the schema + // once all migrations execute. + Room.databaseBuilder( + InstrumentationRegistry.getInstrumentation().targetContext, + AppDatabase::class.java, + TEST_DB + ).addMigrations(*ALL_MIGRATIONS) + .build().apply { + openHelper.writableDatabase + close() + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/io/legado/app/UpdateTest.kt b/app/src/androidTest/java/io/legado/app/UpdateTest.kt new file mode 100755 index 000000000..696eec31e --- /dev/null +++ b/app/src/androidTest/java/io/legado/app/UpdateTest.kt @@ -0,0 +1,54 @@ +package io.legado.app + +import com.google.gson.Gson +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.http.okHttpClient +import io.legado.app.help.update.GithubRelease +import io.legado.app.utils.fromJsonObject +import okhttp3.Request +import org.junit.Assert.assertTrue +import org.junit.Test + +class UpdateTest { + + private val lastReleaseUrl = + "https://api.github.com/repos/gedoor/legado/releases/latest" + + private val lastBetaReleaseUrl = + "https://api.github.com/repos/gedoor/legado/releases/tags/beta" + + @Test + fun updateApp_beta() { + val body = okHttpClient.newCall(Request.Builder().url(lastBetaReleaseUrl).build()).execute() + .body!!.string() + + val releaseList = Gson().fromJsonObject(body) + .getOrElse { + throw NoStackTraceException("获取新版本出错 " + it.localizedMessage) + } + .gitReleaseToAppReleaseInfo() + .sortedByDescending { it.createdAt } + + assertTrue(releaseList.size == 2) + assertTrue(releaseList.all { it.downloadUrl.isNotBlank() }) + assertTrue(releaseList.all { it.versionName.isNotBlank() }) + } + + @Test + fun updateApp() { + val body = okHttpClient.newCall(Request.Builder().url(lastReleaseUrl).build()).execute() + .body!!.string() + + val releaseList = Gson().fromJsonObject(body) + .getOrElse { + throw NoStackTraceException("获取新版本出错 " + it.localizedMessage) + } + .gitReleaseToAppReleaseInfo() + .sortedByDescending { it.createdAt } + + assertTrue(releaseList.size == 1) + assertTrue(releaseList.all { it.downloadUrl.isNotBlank() }) + assertTrue(releaseList.all { it.versionName.isNotBlank() }) + } + +} \ No newline at end of file diff --git a/app/src/debug/res/values-zh/strings.xml b/app/src/debug/res/values-zh/strings.xml new file mode 100755 index 000000000..e4464c88d --- /dev/null +++ b/app/src/debug/res/values-zh/strings.xml @@ -0,0 +1,4 @@ + + 阅读·D + 阅读·D·搜索 + \ No newline at end of file diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100755 index 000000000..ffc41e3cd --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + legado·D + legado·D·search + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100755 index 000000000..1dd7e1920 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,549 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/18PlusList.txt b/app/src/main/assets/18PlusList.txt new file mode 100755 index 000000000..f075bdfc2 --- /dev/null +++ b/app/src/main/assets/18PlusList.txt @@ -0,0 +1,284 @@ +OGN5dS5jb20= +c2cwMC54eXo= +aXRyYWZmaWNuZXQuY29t +eGlhb3FpYW5nNTIw +MTIzeGlhb3FpYW5n +eGlhb3FpYW5neHM= +eGlhb3FpYW5nNTIw +MzM1eHM= +eGN4czk= +eGN4czUyMA== +c2h1YmFvYW4= +c2h1YmFvd2FuZzEyMw== +c2h1YmFvYW4= +aGFpdGFuZzEyMw== +eXV6aGFpd3VsYQ== +cG8xOA== +Ymwtbm92ZWw= +NXRucw== +c2hhb3NodWdl +amluamlzaHV3dQ== +NDJ3Zw== +eWlxdXNodQ== +c2h1YmFvd2FuZzEyMw== +M2hlYmFv +MzNoZWJhbw== +bHVvcWl1enc= +bXlzaHVnZQ== +c3NzeHN3 +eWl0ZQ== +Y3Vpd2VpanV1 +Y3Vpd2VpanV4cw== +Y3Vpd2VpanV4 +eGlhb3FpYW5nd3g= +YXN6dw== +YXN6dzY= +c2FuaGFveHM= +ODdzaHV3dQ== +NDh3eA== +bG9uZ3Rlbmcy +NnF3eA== +bG9uZ3Rlbmd4cw== +aGF4ZHU= +M3R3eA== +aGF4d3g1 +NjZsZXdlbg== +eGJhbnpodQ== +aGR5cA== +ZHliejk= +ZGl5aWJhbnpodTk= +ZGl5aWJhbnpodQ== +ZGl5aWJhbnpodTc= +YnoyMjI= +d29kZWFwaTAwMQ== +dGFuZ3poZWthbg== +YmF4aWFueHM= +eGlhb3NodW9zaGVuemhhbg== +ZGFtb2tl +emh3ZW5wZw== +eXV6aGFpZ2U= +d21wOA== +OXhpYW53ZW4= +bmFucmVudmlw +cmV5b28= +eWZ4aWFvc2h1b2U= +c2Fuaml1enc= +N3Fpbmc3 +cWR4aWFvc2h1bw== +Y2hpbmVzZXpq +MzlzaHViYW8= +a3l4czU= +NTZtcw== +bml1c2hh +bWt4czY= +MjIyMjJ4cw== +OTVkdXNodQ== +YmFuemh1MjI= +d3JsdHh0 +dHVkb3V0eHQ= +cm5neHM= +OTl3ZW5rdQ== +bGFvc2lqaXhz +ZnVzaHV6aGFpMQ== +cG8xOA== +czUyMTc= +c2FuaGFveHM= +NTJrc2h1 +NDhyeA== +ZWNub3ZlbA== +bGllaHVvenc= +eGlhb3FpYW5nd3g= +NTJrc2h1 +NDh3eA== +NTJrc2h1 +MDB1aQ== +MDFieg== +c2h1YmFvMQ== +ZG54aWFvc2h1b2E= +am5zaHViYQ== +MThzaHV3dQ== +bGV4cw== +MzM1eHM= +dXB1 +ZnVndW9kdQ== +ODB0eHQ= +YWFyZWFk +eWlkdWR1MQ== +YmFuemh1d2FuZw== +cWloYW9xaWhhbw== +OHhpYW54cw== +amluamlzaHV3dQ== +d21wOA== +ZXl1c2h1d3U= +NTB4c2Y= +aGF4d3g1 +cG93YW5qdWFu +d2luMTBjaXR5 +eWV5ZXhzdw== +bXlzaHVnZQ== +eGlhbmd0eHN3 +Y3Vpd2VpanV4 +MzY2eHN3 +aHVheXVld2Vua3U= +eW91ZGlhbmxlbg== +c291Nzg= +bGFucm91Mg== +cXFib29r +eW91d3V4cw== +cnVpbGlzYWxl +MzY1bXd3 +ZnV3ZW5o +bGVzYmw= +YXd1Ym9vaw== +bGl5dXhpYW5nMjAyMA== +OTJwb3Bv +ZnVzaHV0dWFu +ODhkYW5tZWk= +ZG14cw== +eXVsaW56aGFueWU= +M2hlYmFv +eGd1YWd1YXhz +ZGl5aWJhbnpodTY= +aXJlYWR4cw== +c2h1YmFvOTY= +ZGl5aWJhbnpodTU1NQ== +c2Fuaml1enc= +N3Fpbmc3 +NjZsZXdlbg== +a3l4czU= +MjIyMjJ4cw== +c2hhb3NodWdl +amlsaW41NQ== +bWt4czY= +amluc2h1bG91 +eGlhbndhbmdz +eWlkdWR1 +cWR0eHQ= +MTZib29rMQ== +am1zaHV3dQ== +MzY2eHN3 +ZHliejk= +c2hvdWRhOA== +ZnlxMTg= +eWlzaHVn +eXV6aGFpd3VsYQ== +MTFiYW56aHU= +MTIzeGlhb3FpYW5n +ZGl5aWJhbnpodTk= +ZGl5aWJhbnpodQ== +MzY2eHN3 +ODdzaHV3dQ== +NnF3eA== +emhlbmh1bnhpYW9zaHVv +bG9uZ3Rlbmc1Mg== +eGlueGluZ3hpYW5nemhpZmE= +ZHliejk= +ZHVvemhla2Fu +MTIzeGlhb3FpYW5n +MzM1eHM= +am1zaHV3dQ== +c2hhb3NodWdl +bGF3ZW54cw== +cnVzaHV3dQ== +MzY2eHN3 +NTB4c2Y= +bGV3ZW41NQ== +aGFpdGFuZzEyMw== +aGViYW81MjA= +bHVvcWl1enc= +c3NzeHN3 +c2h1c2h1d3V4cw== +cm5neHM= +cWR4aWFvc2h1bw== +dHl1ZQ== +Y2hlNDM= +bG9uZ3Rlbmcy +amZ5eHNo +aGV0dTI= +bGFvc2lqaXhz +bG9uZ3Rlbmd4cw== +bGllaHVvenc= +c2h1YmFvYW4= +eHNodW9zaHVv +NTIxZGFubWVp +YmFuemh1MjI= +cWtzaHU= +eWZ4aWFvc2h1b2U= +a3lnc28= +c2h1bG91YmE= +NXRucw== +N3Fpbmc3 +bWlhb2R1NQ== +eXVzaHV3ZW4= +YWFyZWFk +cXRzaHU= +MTdzaHV3dQ== +c2h1YmFvMnM= +YnowMDE= +ZGFtb2dl +MTMxdGI= +aXhpYW9z +bXlzaHVnZQ== +OXhpYW53ZW4= +ZHVvemhla2Fu +MTIwdw== +c2h1c2h1d3U1MjA= +c2h1YmFvMnM= +YWd4c3c= +OTR4c3c= +cG8xOA== +eWFvY2hpeHM= +eGlhb3FpYW5neHM= +Ym9va2Js +c2Fuaml1eHM= +d29kZXNodWJhbw== +em9uZ2NhaXhpYW9zaHVvMg== +OWI4OTEzOTRkZjVi +MThub3ZlbA== +YWFib29r +YjF0eHQ= +eXVjYWl6dw== +Yzl0eHQ= +ZGl5aWJhbnpodTU1NQ== +MzBtYw== +eGlueXVzaHV3dQ== +c2h1YmFvd2FuZzEyMw== +YWd4cw== +YmlxdWdlbmw= +c2hpcWlzaHV3dQ== +c2lsdWtl +ZGl5aWJhbnpodTg= +ZGl5aWJhbnpodTk= +aGV0dW54cw== +OTl3ZW5rdQ== +aGFpdGFuZ3NodXd1 +OTd5ZA== +eXV6aGFpd3UxMQ== +Y3Vpd2VpanV4cw== +Y2JpcXU= +NTIxZGFubWVp +c2h1YmFvMzM= +c2FuaGFvMQ== +dGlhbm1lbmd3ZW5rdQ== +eXVzaHV3dTUyMA== +c2h1YmFvMjIy +c2h1YmFvd2FuZzEyMw== +eXVib29r +Y2JpcXU= +MWxld2Vu +MTV4c3c= +eG5jd3h3 +c2h1YmFvd2FuZzEyMw== +c2FuaGFveHM= +eXV3YW5nc2hl +YmlxdXRz +bGFtZWl4cw== +eGJhbnpodQ== +cWR4aWFvc2h1bw== +bWh0bGE= +OTl3ZW5rdQ== +eGlhb3FpYW5nNTIw +dGlhbm1lbmd3ZW5rdQ== +YWlmdXNodQ== +bWlhb2R1NQ== +bWlmZW5neHM= \ No newline at end of file diff --git a/app/src/main/assets/LICENSE.md b/app/src/main/assets/LICENSE.md new file mode 100755 index 000000000..7fd24371c --- /dev/null +++ b/app/src/main/assets/LICENSE.md @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies of this license document, +but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Legado Copyright (C) 2022 gedoor + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/app/src/main/assets/bg/午后沙滩.jpg b/app/src/main/assets/bg/午后沙滩.jpg new file mode 100755 index 0000000000000000000000000000000000000000..e523ff10f6614496fdc9dfb364b89eb0220f0118 GIT binary patch literal 8100 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/山水墨影.jpg b/app/src/main/assets/bg/山水墨影.jpg new file mode 100755 index 0000000000000000000000000000000000000000..2b1d51f8d0a5abf71aeb51e73ebb35a6336f3df3 GIT binary patch literal 6462 zcmeI0Z%k8H6u{4YeYKCmXxQIY1T=r}2Q;>76qsWFq| zl9c2ZY>F+>F?0TGN7}r!MBAK&8S`GsQC+G%-Mz$3m&|vnIV=c?jEahWF4`(d)|?bu zO3uHQ;5m>4BXE}tL;_AC28jgELmZYG5*QQ08F-@!_on@A11)ebcnhKo1US(kqT*>$ z*O;UJp%c&v=mc~EIsu)4PCzH16VM6h1atyA0iA$OKqv735x`N@e-Y6V^5$^@qx4g3+@c{0jT+q^+Jtr@hn8t}j76|DJ5oZ-*4i*3EQs1MDbItZRr{P#{+^Vt z><$V6V}J%i+3A);=)ro_%q%GZSyoaV`=;!(Exx7@Odhy)I%A!KPN@IX-iq}(KpE0N zgA~;t;L(v1BB|8G}di1wz2DiD)7-d$h&C)gd2t=#iEyH(O`F z-ccT3k-^Yb2k=-#>>Ue7Qgt*JN2}a+5uH#tE>>yw@escuL!j6xXE8<)+TcVwj3NQV z1xkv@RX32e9Ct2uaZ5riUXUrn>8n*yw2?}qk=c^vW?rpkXQ^@EO6XK2=2(Csg$_-m zW*Wa{CCEY~o>LpJl5sBU1Eo|@1eYy|aDpI!*&U#&&}1_TGMM`Nv{$ffmfy_UKwv`4 z$I~3|&TT{`l|AZt@Eo7in`;3-OB#=3-~ZpWbYGv@B_J)s4n8H}cP# z+mB{mJ$T@^O&EC4yV{Yz;lSObtBz&1ds%&3wqIlQ9e;T8?X0z@yoKe%-jdZFtm55e zF%5Mw-RX6c(s$GAnfcz^#q0a87FoNs4A{tgMkJ%RYFpVN<^nLasV%vu>0wv#z5@g8 z?Kffm&bl`st@q5paA9i2_h@jx=l;seWv5DCoV_(V;o@h^$GUUZf|ksxACAq;yRf0@ zup@89gC6hw_RrrzkclE*3}WDMdOj+ITP}&SoU*5&RH5o9Nkb0&ak%Ql%iC&;-SxGi zGT`cwPNWw58!x!;6vn2N*^V^7Hykt6IM7g87pat|)%ZKDxg~?KKMH$lRml}|dsSsi z<*s{!il9zY#nYm1M$Fe`*2hCtuf3n#aa1~6&|O=dkew~w&Tg&w=x3WE6jiF1zk6_f z@1p*LU%FztvwNRjvF4`L)K$bM9?Ct^+}P=wjsUyUhteN(?Zr-FC3fpauLEmFf>Jti o6F5S0;FksN-P@l}NOV^$tqTIWSfszY4tF2!*1)V+8G_&Z30a<)761SM literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/山水画.jpg b/app/src/main/assets/bg/山水画.jpg new file mode 100755 index 0000000000000000000000000000000000000000..f272fa881c6d7bf5cada8a64397c9d31a81f4983 GIT binary patch literal 14244 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 ak`hg_Ys7ypaI(ifDbuFw=7-%s#{Une5M9Io literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/护眼漫绿.jpg b/app/src/main/assets/bg/护眼漫绿.jpg new file mode 100755 index 0000000000000000000000000000000000000000..a8d79eff1b315858f921151745b01d8a026bb88b GIT binary patch literal 92965 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�B&Iui@`x`wGsk8S$U7)wmg_%3Y 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/新羊皮纸.jpg b/app/src/main/assets/bg/新羊皮纸.jpg new file mode 100755 index 0000000000000000000000000000000000000000..20782a9a954a18fcfc11219ccd84e0be54857d74 GIT binary patch literal 26470 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/深宫魅影.jpg b/app/src/main/assets/bg/深宫魅影.jpg new file mode 100755 index 0000000000000000000000000000000000000000..4529b4132b3138fe06a0cc751f23e51ac16db105 GIT binary patch literal 8452 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 DeVQCl literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/清新时光.jpg b/app/src/main/assets/bg/清新时光.jpg new file mode 100755 index 0000000000000000000000000000000000000000..da8f40a4d1df4581dd492fd67b293b624e392c37 GIT binary patch literal 8462 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

    9Z{#oiw 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 -$-YjAuLOKo-*x2dkoPwFzIfu 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%0D(<$?3&2n}a`}_rgi#jy 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

    BXEy73y@+dK7&t7@ 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 literal 0 HcmV?d00001 diff --git a/app/src/main/assets/bg/羊皮纸2.jpg b/app/src/main/assets/bg/羊皮纸2.jpg new file mode 100755 index 0000000000000000000000000000000000000000..3e46c8e0f7364d46772b70b115811ab1d61f73ff GIT binary patch literal 319831 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