├── .dockerignore ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug-report--问题反馈-.md └── workflows │ ├── Dockerfile │ ├── Openj9-Dockerfile │ ├── build.yml │ ├── pull-request.yml │ ├── release-github.yml │ ├── release-openj9.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── Dockerfile.source ├── LICENSE ├── README.md ├── UPDATELOG.md ├── assets ├── linux │ └── reader.png ├── mac │ └── reader.icns └── windows │ └── reader.ico ├── build.gradle.kts ├── build.sh ├── cli.gradle ├── doc.md ├── docker-compose.yaml ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── imgs ├── 1.jpg ├── 10.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg ├── 9.jpg └── mpcode.png ├── nixpacks.toml ├── preview.md ├── reader.sh ├── server ├── bin │ ├── shutdown.cmd │ ├── shutdown.sh │ ├── startup.cmd │ └── startup.sh └── conf │ └── application.properties ├── settings.gradle ├── src ├── lib │ ├── rhino-1.7.13-1.jar │ └── xmlpull-1.1.3.1.jar ├── main │ ├── java │ │ ├── com │ │ │ └── htmake │ │ │ │ └── reader │ │ │ │ ├── ReaderApplication.kt │ │ │ │ ├── ReaderUIApplication.kt │ │ │ │ ├── SpringEvent.java │ │ │ │ ├── api │ │ │ │ ├── ReturnData.kt │ │ │ │ ├── YueduApi.kt │ │ │ │ └── controller │ │ │ │ │ ├── BaseController.kt │ │ │ │ │ ├── BookController.kt │ │ │ │ │ ├── BookSourceController.kt │ │ │ │ │ ├── BookmarkController.kt │ │ │ │ │ ├── ReplaceRuleController.kt │ │ │ │ │ ├── RssSourceController.kt │ │ │ │ │ ├── UserController.kt │ │ │ │ │ └── WebdavController.kt │ │ │ │ ├── config │ │ │ │ ├── AppConfig.kt │ │ │ │ └── BookConfig.kt │ │ │ │ ├── entity │ │ │ │ ├── BasicError.kt │ │ │ │ ├── Size.kt │ │ │ │ └── User.kt │ │ │ │ ├── init │ │ │ │ └── appCtx.kt │ │ │ │ ├── utils │ │ │ │ ├── Ext.kt │ │ │ │ ├── SpringContextUtils.java │ │ │ │ └── VertExt.kt │ │ │ │ └── verticle │ │ │ │ └── RestVerticle.kt │ │ ├── io │ │ │ └── legado │ │ │ │ └── app │ │ │ │ ├── README.md │ │ │ │ ├── constant │ │ │ │ ├── Action.kt │ │ │ │ ├── AppConst.kt │ │ │ │ ├── AppPattern.kt │ │ │ │ ├── BookType.kt │ │ │ │ ├── DeepinkBookSource.kt │ │ │ │ ├── PreferKey.kt │ │ │ │ ├── RSSKeywords.kt │ │ │ │ └── Status.kt │ │ │ │ ├── data │ │ │ │ └── entities │ │ │ │ │ ├── BaseBook.kt │ │ │ │ │ ├── BaseSource.kt │ │ │ │ │ ├── Book.kt │ │ │ │ │ ├── BookChapter.kt │ │ │ │ │ ├── BookGroup.kt │ │ │ │ │ ├── BookSource.kt │ │ │ │ │ ├── Bookmark.kt │ │ │ │ │ ├── Cache.kt │ │ │ │ │ ├── Cookie.kt │ │ │ │ │ ├── ReplaceRule.kt │ │ │ │ │ ├── RssArticle.kt │ │ │ │ │ ├── RssSource.kt │ │ │ │ │ ├── SearchBook.kt │ │ │ │ │ ├── SearchKeyword.kt │ │ │ │ │ ├── SearchResult.kt │ │ │ │ │ ├── TxtTocRule.kt │ │ │ │ │ └── rule │ │ │ │ │ ├── BookInfoRule.kt │ │ │ │ │ ├── BookListRule.kt │ │ │ │ │ ├── ContentRule.kt │ │ │ │ │ ├── ExploreRule.kt │ │ │ │ │ ├── SearchRule.kt │ │ │ │ │ └── TocRule.kt │ │ │ │ ├── exception │ │ │ │ ├── ConcurrentException.kt │ │ │ │ ├── ContentEmptyException.kt │ │ │ │ ├── NoStackTraceException.kt │ │ │ │ ├── RegexTimeoutException.kt │ │ │ │ └── TocEmptyException.kt │ │ │ │ ├── help │ │ │ │ ├── BookHelp.kt │ │ │ │ ├── CacheManager.kt │ │ │ │ ├── DefaultData.kt │ │ │ │ ├── EncodingDetectHelp.java │ │ │ │ ├── JsExtensions.kt │ │ │ │ ├── coroutine │ │ │ │ │ ├── CompositeCoroutine.kt │ │ │ │ │ ├── Coroutine.kt │ │ │ │ │ └── CoroutineContainer.kt │ │ │ │ └── http │ │ │ │ │ ├── AjaxWebView.kt │ │ │ │ │ ├── ByteConverter.kt │ │ │ │ │ ├── CookieStore.kt │ │ │ │ │ ├── CoroutinesCallAdapterFactory.kt │ │ │ │ │ ├── EncodeConverter.kt │ │ │ │ │ ├── HttpHelper.kt │ │ │ │ │ ├── OkHttpUtils.kt │ │ │ │ │ ├── RequestMethod.kt │ │ │ │ │ ├── Res.kt │ │ │ │ │ ├── SSLHelper.kt │ │ │ │ │ ├── StrResponse.kt │ │ │ │ │ └── api │ │ │ │ │ └── CookieManager.kt │ │ │ │ ├── lib │ │ │ │ └── icu4j │ │ │ │ │ ├── CharsetDetector.java │ │ │ │ │ ├── CharsetMatch.java │ │ │ │ │ ├── CharsetRecog_2022.java │ │ │ │ │ ├── CharsetRecog_UTF8.java │ │ │ │ │ ├── CharsetRecog_Unicode.java │ │ │ │ │ ├── CharsetRecog_mbcs.java │ │ │ │ │ ├── CharsetRecog_sbcs.java │ │ │ │ │ └── CharsetRecognizer.java │ │ │ │ ├── model │ │ │ │ ├── Debug.kt │ │ │ │ ├── DebugLog.kt │ │ │ │ ├── Debugger.kt │ │ │ │ ├── README.md │ │ │ │ ├── analyzeRule │ │ │ │ │ ├── AnalyzeByJSonPath.kt │ │ │ │ │ ├── AnalyzeByJSoup.kt │ │ │ │ │ ├── AnalyzeByRegex.kt │ │ │ │ │ ├── AnalyzeByXPath.kt │ │ │ │ │ ├── AnalyzeRule.kt │ │ │ │ │ ├── AnalyzeUrl.kt │ │ │ │ │ ├── QueryTTF.java │ │ │ │ │ ├── RuleAnalyzer.kt │ │ │ │ │ ├── RuleData.kt │ │ │ │ │ └── RuleDataInterface.kt │ │ │ │ ├── localBook │ │ │ │ │ ├── CbzFile.kt │ │ │ │ │ ├── EpubFile.kt │ │ │ │ │ ├── LocalBook.kt │ │ │ │ │ ├── TextFile.kt │ │ │ │ │ └── UmdFile.kt │ │ │ │ ├── rss │ │ │ │ │ ├── Rss.kt │ │ │ │ │ ├── RssParserByRule.kt │ │ │ │ │ └── RssParserDefault.kt │ │ │ │ └── webBook │ │ │ │ │ ├── BookChapterList.kt │ │ │ │ │ ├── BookContent.kt │ │ │ │ │ ├── BookInfo.kt │ │ │ │ │ ├── BookList.kt │ │ │ │ │ └── WebBook.kt │ │ │ │ └── utils │ │ │ │ ├── ACache.kt │ │ │ │ ├── AnkoHelps.kt │ │ │ │ ├── Base64.java │ │ │ │ ├── EncoderUtils.kt │ │ │ │ ├── EncodingDetect.kt │ │ │ │ ├── FileExtensions.kt │ │ │ │ ├── FilesUtil.kt │ │ │ │ ├── GsonExtensions.kt │ │ │ │ ├── HtmlFormatter.kt │ │ │ │ ├── JsonExtensions.kt │ │ │ │ ├── JsoupExtensions.kt │ │ │ │ ├── LogUtils.kt │ │ │ │ ├── MD5Utils.kt │ │ │ │ ├── NetworkUtils.kt │ │ │ │ ├── SourceAnalyzer.kt │ │ │ │ ├── StringExtensions.kt │ │ │ │ ├── StringUtils.kt │ │ │ │ ├── TextUtils.java │ │ │ │ ├── ThrowableExtensions.kt │ │ │ │ ├── UTF8BOMFighter.kt │ │ │ │ ├── Utf8BomUtils.kt │ │ │ │ └── ZipUtils.kt │ │ ├── me │ │ │ └── ag2s │ │ │ │ ├── epublib │ │ │ │ ├── Constants.java │ │ │ │ ├── browsersupport │ │ │ │ │ ├── NavigationEvent.java │ │ │ │ │ ├── NavigationEventListener.java │ │ │ │ │ ├── NavigationHistory.java │ │ │ │ │ ├── Navigator.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── domain │ │ │ │ │ ├── Author.java │ │ │ │ │ ├── Date.java │ │ │ │ │ ├── EpubBook.java │ │ │ │ │ ├── EpubResourceProvider.java │ │ │ │ │ ├── FileResourceProvider.java │ │ │ │ │ ├── Guide.java │ │ │ │ │ ├── GuideReference.java │ │ │ │ │ ├── Identifier.java │ │ │ │ │ ├── LazyResource.java │ │ │ │ │ ├── LazyResourceProvider.java │ │ │ │ │ ├── ManifestItemProperties.java │ │ │ │ │ ├── ManifestItemRefProperties.java │ │ │ │ │ ├── ManifestProperties.java │ │ │ │ │ ├── MediaType.java │ │ │ │ │ ├── MediaTypes.java │ │ │ │ │ ├── Metadata.java │ │ │ │ │ ├── Relator.java │ │ │ │ │ ├── Resource.java │ │ │ │ │ ├── ResourceInputStream.java │ │ │ │ │ ├── ResourceReference.java │ │ │ │ │ ├── Resources.java │ │ │ │ │ ├── Spine.java │ │ │ │ │ ├── SpineReference.java │ │ │ │ │ ├── TOCReference.java │ │ │ │ │ ├── TableOfContents.java │ │ │ │ │ └── TitledResourceReference.java │ │ │ │ ├── epub │ │ │ │ │ ├── BookProcessor.java │ │ │ │ │ ├── BookProcessorPipeline.java │ │ │ │ │ ├── DOMUtil.java │ │ │ │ │ ├── EpubProcessorSupport.java │ │ │ │ │ ├── EpubReader.java │ │ │ │ │ ├── EpubWriter.java │ │ │ │ │ ├── HtmlProcessor.java │ │ │ │ │ ├── NCXDocumentV2.java │ │ │ │ │ ├── NCXDocumentV3.java │ │ │ │ │ ├── PackageDocumentBase.java │ │ │ │ │ ├── PackageDocumentMetadataReader.java │ │ │ │ │ ├── PackageDocumentMetadataWriter.java │ │ │ │ │ ├── PackageDocumentReader.java │ │ │ │ │ ├── PackageDocumentWriter.java │ │ │ │ │ └── ResourcesLoader.java │ │ │ │ └── util │ │ │ │ │ ├── CollectionUtil.java │ │ │ │ │ ├── IOUtil.java │ │ │ │ │ ├── NoCloseOutputStream.java │ │ │ │ │ ├── NoCloseWriter.java │ │ │ │ │ ├── ResourceUtil.java │ │ │ │ │ ├── StringUtil.java │ │ │ │ │ └── commons │ │ │ │ │ └── io │ │ │ │ │ ├── BOMInputStream.java │ │ │ │ │ ├── ByteOrderMark.java │ │ │ │ │ ├── IOConsumer.java │ │ │ │ │ ├── ProxyInputStream.java │ │ │ │ │ ├── XmlStreamReader.java │ │ │ │ │ └── XmlStreamReaderException.java │ │ │ │ └── umdlib │ │ │ │ ├── domain │ │ │ │ ├── UmdBook.java │ │ │ │ ├── UmdChapters.java │ │ │ │ ├── UmdCover.java │ │ │ │ ├── UmdEnd.java │ │ │ │ └── UmdHeader.java │ │ │ │ ├── tool │ │ │ │ ├── StreamReader.java │ │ │ │ ├── UmdUtils.java │ │ │ │ └── WrapOutputStream.java │ │ │ │ └── umd │ │ │ │ └── UmdReader.java │ │ └── org │ │ │ └── kxml2 │ │ │ ├── io │ │ │ ├── KXmlParser.java │ │ │ └── KXmlSerializer.java │ │ │ ├── kdom │ │ │ ├── Document.java │ │ │ ├── Element.java │ │ │ └── Node.java │ │ │ └── wap │ │ │ ├── Wbxml.java │ │ │ ├── WbxmlParser.java │ │ │ ├── WbxmlSerializer.java │ │ │ ├── syncml │ │ │ └── SyncML.java │ │ │ ├── wml │ │ │ └── Wml.java │ │ │ └── wv │ │ │ └── WV.java │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── org.xmlpull.v1.XmlPullParserFactory │ │ ├── application-prod.yml │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── defaultData │ │ └── txtTocRule.json │ │ ├── dtd │ │ ├── openebook.org │ │ │ └── dtds │ │ │ │ └── oeb-1.2 │ │ │ │ ├── oeb12.ent │ │ │ │ └── oebpkg12.dtd │ │ ├── www.daisy.org │ │ │ └── z3986 │ │ │ │ └── 2005 │ │ │ │ └── ncx-2005-1.dtd │ │ └── www.w3.org │ │ │ └── TR │ │ │ ├── ruby │ │ │ └── xhtml-ruby-1.mod │ │ │ ├── xhtml-modularization │ │ │ └── DTD │ │ │ │ ├── xhtml-arch-1.mod │ │ │ │ ├── xhtml-attribs-1.mod │ │ │ │ ├── xhtml-base-1.mod │ │ │ │ ├── xhtml-bdo-1.mod │ │ │ │ ├── xhtml-blkphras-1.mod │ │ │ │ ├── xhtml-blkpres-1.mod │ │ │ │ ├── xhtml-blkstruct-1.mod │ │ │ │ ├── xhtml-charent-1.mod │ │ │ │ ├── xhtml-csismap-1.mod │ │ │ │ ├── xhtml-datatypes-1.mod │ │ │ │ ├── xhtml-datatypes-1.mod.1 │ │ │ │ ├── xhtml-edit-1.mod │ │ │ │ ├── xhtml-events-1.mod │ │ │ │ ├── xhtml-form-1.mod │ │ │ │ ├── xhtml-framework-1.mod │ │ │ │ ├── xhtml-hypertext-1.mod │ │ │ │ ├── xhtml-image-1.mod │ │ │ │ ├── xhtml-inlphras-1.mod │ │ │ │ ├── xhtml-inlpres-1.mod │ │ │ │ ├── xhtml-inlstruct-1.mod │ │ │ │ ├── xhtml-inlstyle-1.mod │ │ │ │ ├── xhtml-lat1.ent │ │ │ │ ├── xhtml-link-1.mod │ │ │ │ ├── xhtml-list-1.mod │ │ │ │ ├── xhtml-meta-1.mod │ │ │ │ ├── xhtml-notations-1.mod │ │ │ │ ├── xhtml-object-1.mod │ │ │ │ ├── xhtml-param-1.mod │ │ │ │ ├── xhtml-pres-1.mod │ │ │ │ ├── xhtml-qname-1.mod │ │ │ │ ├── xhtml-script-1.mod │ │ │ │ ├── xhtml-special.ent │ │ │ │ ├── xhtml-ssismap-1.mod │ │ │ │ ├── xhtml-struct-1.mod │ │ │ │ ├── xhtml-style-1.mod │ │ │ │ ├── xhtml-symbol.ent │ │ │ │ ├── xhtml-symbol.ent.1 │ │ │ │ ├── xhtml-table-1.mod │ │ │ │ ├── xhtml-text-1.mod │ │ │ │ └── xhtml11-model-1.mod │ │ │ ├── xhtml1 │ │ │ └── DTD │ │ │ │ ├── xhtml-lat1.ent │ │ │ │ ├── xhtml-special.ent │ │ │ │ ├── xhtml-symbol.ent │ │ │ │ ├── xhtml1-strict.dtd │ │ │ │ └── xhtml1-transitional.dtd │ │ │ └── xhtml11 │ │ │ └── DTD │ │ │ └── xhtml11.dtd │ │ ├── epub │ │ ├── chapter.html │ │ ├── cover.html │ │ ├── fonts.css │ │ ├── intro.html │ │ ├── logo.png │ │ └── main.css │ │ ├── icons │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ └── 64x64.png │ │ ├── images │ │ └── loading.gif │ │ └── logback-spring.xml └── test │ └── java │ └── com │ └── htmake │ └── reader │ └── ReaderApplicationTests.java ├── vetur.config.js └── web ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public ├── bg │ ├── 午后沙滩.jpg │ ├── 宁静夜色.jpg │ ├── 山水墨影.jpg │ ├── 山水画.jpg │ ├── 护眼漫绿.jpg │ ├── 新羊皮纸.jpg │ ├── 明媚倾城.jpg │ ├── 深宫魅影.jpg │ ├── 清新时光.jpg │ ├── 羊皮纸1.jpg │ ├── 羊皮纸2.jpg │ ├── 羊皮纸3.jpg │ ├── 羊皮纸4.jpg │ └── 边彩画布.jpg ├── bookSourceDebug │ ├── index.css │ ├── index.html │ └── index.js ├── browsertest.html ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-maskable-192x192.png │ │ ├── android-chrome-maskable-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html ├── robots.txt └── sw.js ├── src ├── App.vue ├── assets │ ├── fonts │ │ ├── iconfont.css │ │ ├── iconfont.woff │ │ ├── reader-iconfont.ttf │ │ ├── reader-iconfont.woff │ │ └── reader-iconfont.woff2 │ ├── imgs │ │ ├── github.png │ │ ├── github2.png │ │ ├── mpcode.jpg │ │ ├── noCover.jpeg │ │ ├── noImage.png │ │ └── themes │ │ │ ├── body_0.png │ │ │ ├── body_1.png │ │ │ ├── body_2.png │ │ │ ├── body_3.png │ │ │ ├── body_5.png │ │ │ ├── body_6.png │ │ │ ├── content_0.png │ │ │ ├── content_1.png │ │ │ ├── content_2.png │ │ │ ├── content_3.png │ │ │ ├── content_5.png │ │ │ ├── content_6.png │ │ │ ├── popup_0.png │ │ │ ├── popup_1.png │ │ │ ├── popup_2.png │ │ │ ├── popup_3.png │ │ │ ├── popup_5.png │ │ │ └── popup_6.png │ └── logo.png ├── components │ ├── AddUser.vue │ ├── BookGroup.vue │ ├── BookInfo.vue │ ├── BookManage.vue │ ├── BookShelf.vue │ ├── BookSource.vue │ ├── Bookmark.vue │ ├── BookmarkForm.vue │ ├── Content.vue │ ├── Explore.vue │ ├── LocalStore.vue │ ├── MPCode.vue │ ├── PopCatalog.vue │ ├── ReadSettings.vue │ ├── ReplaceRule.vue │ ├── ReplaceRuleForm.vue │ ├── RssArticle.vue │ ├── RssArticleList.vue │ ├── RssSourceList.vue │ ├── SearchBookContent.vue │ ├── UserManage.vue │ └── WebDAV.vue ├── main.js ├── plugins │ ├── animate.js │ ├── axios.js │ ├── cache.js │ ├── chinese.js │ ├── config.js │ ├── element.js │ ├── eventBus.js │ ├── helper.js │ ├── jump.js │ ├── md5.js │ ├── safe-json-stringify.js │ └── vuex.js ├── registerServiceWorker.js ├── router │ └── index.js └── views │ ├── Index.vue │ └── Reader.vue └── vue.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | 35 | .vertx 36 | out 37 | 38 | #/src/main/resources/application-prod.yml 39 | /src/main/resources/application-default.yml 40 | 41 | /src/main/resources/web 42 | /storage* 43 | /logs 44 | /bin 45 | /file-uploads 46 | node_modules 47 | /reader-assets -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.java linguist-language=Kotlin -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report--问题反馈-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report (问题反馈) 3 | about: 描述你在使用中遇到的问题(issue语言:1. 中文;2. 英文) 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **为避免无效问题和冗余问题,提问前请确认** 11 | 1. 你确定Google不能解决你的问题 12 | 2. 你确定已有的issue不能解决你的问题 13 | 3. 你确定issue的title按照格式如下:[web/simple-web/server]:description 14 | 15 | **Describe the bug 描述你遇到的问题** 16 | A clear and concise description of what the bug is. 简洁有效的说明。 17 | 18 | **To Reproduce 如何重现问题** 19 | Steps to reproduce the behavior: 把你遇到的问题的发生步骤替换掉下面的内容: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior 期待修复的效果** 26 | A clear and concise description of what you expected to happen. 简单描述。 27 | 28 | **Screenshots 如有必要,可以截图说明** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **版本说明** 32 | - OS: [e.g. win] 说明操作系统 33 | - Deploy Method 说明软件部署方式 34 | - Program Version 说明软件版本 35 | - Browser [e.g. chrome, safari] 说明终端、浏览器型号 36 | 37 | **Additional context 其他说明** 38 | Add any other context about the problem here. 添加你认为有必要的内容,否则不写。 39 | -------------------------------------------------------------------------------- /.github/workflows/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | # Install base packages 3 | RUN \ 4 | # apk update; \ 5 | # apk upgrade; \ 6 | # Add CA certs tini tzdata 7 | apk add --no-cache ca-certificates tini tzdata; \ 8 | update-ca-certificates; \ 9 | # Clean APK cache 10 | rm -rf /var/cache/apk/*; 11 | 12 | # 时区 13 | ENV TZ=Asia/Shanghai 14 | 15 | EXPOSE 8080 16 | ENTRYPOINT ["/sbin/tini", "--"] 17 | ADD ./reader.jar /app/bin/reader.jar 18 | CMD ["java", "-jar", "/app/bin/reader.jar" ] 19 | -------------------------------------------------------------------------------- /.github/workflows/Openj9-Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ibm-semeru-runtimes:open-8u332-b09-jre 2 | # Install base packages 3 | RUN \ 4 | apt-get update; \ 5 | apt-get install -y ca-certificates tini tzdata; \ 6 | update-ca-certificates; \ 7 | # Clean apt cache 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | # 时区 11 | ENV TZ=Asia/Shanghai 12 | 13 | EXPOSE 8080 14 | ENTRYPOINT ["/usr/bin/tini", "--"] 15 | ADD ./reader.jar /app/bin/reader.jar 16 | CMD ["java", "-jar", "/app/bin/reader.jar" ] 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Image 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | if: github.repository == 'hectorqin/reader' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Publish to Registry 13 | uses: elgohr/Publish-Docker-Github-Action@master 14 | with: 15 | name: hectorqin/reader-basic 16 | username: ${{ secrets.DOCKER_USERNAME }} 17 | password: ${{ secrets.DOCKER_PASSWORD }} 18 | snapshot: true 19 | tags: "test" 20 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Check 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, reopened, labeled] 6 | branches: 7 | - master 8 | 9 | concurrency: 10 | group: ${{ github.head_ref }} 11 | cancel-in-progress: true 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | jobs: 18 | docker: 19 | if: github.repository == 'hectorqin/reader' 20 | runs-on: ubuntu-latest 21 | steps: 22 | - 23 | name: Checkout 24 | uses: actions/checkout@v2 25 | with: 26 | ref: ${{ github.event.pull_request.base.sha }} 27 | clean: false 28 | - 29 | name: Setup node 30 | uses: actions/setup-node@v2 31 | with: 32 | node-version: '14' 33 | - 34 | name: Build web 35 | run: cd web && npm install && npm run build 36 | - 37 | name: Setup Java 38 | uses: actions/setup-java@v2 39 | with: 40 | distribution: 'temurin' 41 | java-version: '8' 42 | cache: 'gradle' 43 | - 44 | name: Build Java 45 | run: 46 | mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar -------------------------------------------------------------------------------- /.github/workflows/release-github.yml: -------------------------------------------------------------------------------- 1 | name: Publish Github Releases 2 | 3 | on: 4 | # push: 5 | # tags: 6 | # - 'v**' 7 | # branches: 8 | # - master 9 | workflow_dispatch: 10 | 11 | jobs: 12 | buildRelease: 13 | if: github.repository == 'hectorqin/reader' 14 | name: "Build And Release" 15 | runs-on: macos-11 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@v2 20 | - 21 | name: Setup node 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: '14' 25 | - 26 | name: Build web 27 | run: cd web && npm install && npm run build && mv ./dist ../src/main/resources/web 28 | - 29 | name: Setup Java 30 | uses: actions/setup-java@v2 31 | with: 32 | distribution: 'temurin' 33 | java-version: '11' 34 | cache: 'gradle' 35 | - 36 | name: Build MacOS package 37 | run: 38 | JAVAFX_PLATFORM=mac ./gradlew packageReaderMac 39 | - 40 | name: Build Linux package 41 | run: 42 | JAVAFX_PLATFORM=linux ./gradlew packageReaderLinux 43 | - 44 | name: Build Windows package 45 | run: 46 | JAVAFX_PLATFORM=win ./gradlew packageReaderWin 47 | - 48 | name: Build server jar 49 | run: 50 | rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info 51 | - 52 | name: Show files. 53 | run: | 54 | echo Showing current directory: 55 | ls 56 | echo Showing ./target directory: 57 | ls ./build 58 | echo Showing ./target directory: 59 | ls ./build/libs 60 | - 61 | name: Pre Release 62 | if: ${{contains(github.ref, 'master')}} 63 | uses: "marvinpinto/action-automatic-releases@latest" 64 | with: 65 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 66 | automatic_release_tag: "latest" 67 | prerelease: true 68 | title: "Development Build" 69 | files: | 70 | ./build/*.pkg 71 | ./build/*.zip 72 | ./build/libs/*.jar 73 | - 74 | name: Tagged Release 75 | if: ${{contains(github.ref, 'v')}} 76 | uses: "marvinpinto/action-automatic-releases@latest" 77 | with: 78 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 79 | prerelease: false 80 | files: | 81 | ./build/*.pkg 82 | ./build/*.zip 83 | ./build/libs/*.jar -------------------------------------------------------------------------------- /.github/workflows/release-openj9.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Multi-Platform Images Using Openj9 2 | 3 | on: 4 | # push: 5 | # tags: 6 | # - 'v**' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | docker: 11 | if: github.repository == 'hectorqin/reader' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v2 17 | - 18 | name: Setup node 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '14' 22 | - 23 | name: Build web 24 | run: cd web && npm install && npm run build 25 | - 26 | name: Setup Java 27 | uses: actions/setup-java@v2 28 | with: 29 | distribution: 'adopt-openj9' 30 | java-version: '8' 31 | cache: 'gradle' 32 | - 33 | name: Build Java 34 | run: 35 | mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar 36 | - 37 | name: Docker meta 38 | id: meta 39 | uses: docker/metadata-action@v3 40 | with: 41 | # list of Docker images to use as base name for tags 42 | images: | 43 | hectorqin/reader-basic 44 | # generate Docker tags based on the following events/attributes 45 | flavor: | 46 | latest=false 47 | prefix=openj9-,onlatest=true 48 | suffix= 49 | tags: | 50 | type=semver,pattern={{version}} 51 | type=raw,value=latest,enable=${{ !contains(github.ref, 'beta') }} 52 | - 53 | name: Set up QEMU 54 | uses: docker/setup-qemu-action@v1 55 | - 56 | name: Set up Docker Buildx 57 | uses: docker/setup-buildx-action@v1 58 | - 59 | name: Login to DockerHub 60 | uses: docker/login-action@v1 61 | with: 62 | username: ${{ secrets.DOCKER_USERNAME }} 63 | password: ${{ secrets.DOCKER_PASSWORD }} 64 | - 65 | name: Build and push 66 | uses: docker/build-push-action@v2 67 | with: 68 | context: . 69 | file: ./.github/workflows/Openj9-Dockerfile 70 | push: true 71 | tags: ${{ steps.meta.outputs.tags }} 72 | labels: ${{ steps.meta.outputs.labels }} 73 | platforms: | 74 | linux/amd64 75 | linux/arm64/v8 76 | linux/ppc64le 77 | linux/s390x -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Multi-Platform Images 2 | 3 | on: 4 | # push: 5 | # tags: 6 | # - 'v**' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | docker: 11 | if: github.repository == 'hectorqin/reader' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v2 17 | - 18 | name: Setup node 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '14' 22 | - 23 | name: Build web 24 | run: cd web && npm install && npm run build 25 | - 26 | name: Setup Java 27 | uses: actions/setup-java@v2 28 | with: 29 | distribution: 'temurin' 30 | java-version: '8' 31 | cache: 'gradle' 32 | - 33 | name: Build Java 34 | run: 35 | mv ./web/dist ./src/main/resources/web && rm src/main/java/com/htmake/reader/ReaderUIApplication.kt && gradle -b cli.gradle assemble --info && mv ./build/libs/*.jar ./reader.jar 36 | - 37 | name: Docker meta 38 | id: meta 39 | uses: docker/metadata-action@v3 40 | with: 41 | # list of Docker images to use as base name for tags 42 | images: | 43 | hectorqin/reader-basic 44 | # generate Docker tags based on the following events/attributes 45 | tags: | 46 | type=semver,pattern={{version}} 47 | type=raw,value=latest,enable=${{ !contains(github.ref, 'beta') }} 48 | - 49 | name: Set up QEMU 50 | uses: docker/setup-qemu-action@v1 51 | - 52 | name: Set up Docker Buildx 53 | uses: docker/setup-buildx-action@v1 54 | - 55 | name: Login to DockerHub 56 | uses: docker/login-action@v1 57 | with: 58 | username: ${{ secrets.DOCKER_USERNAME }} 59 | password: ${{ secrets.DOCKER_PASSWORD }} 60 | - 61 | name: Build and push 62 | uses: docker/build-push-action@v2 63 | with: 64 | context: . 65 | file: ./.github/workflows/Dockerfile 66 | push: true 67 | tags: ${{ steps.meta.outputs.tags }} 68 | labels: ${{ steps.meta.outputs.labels }} 69 | platforms: | 70 | linux/amd64 71 | linux/arm64 72 | linux/arm/v6 73 | linux/arm/v7 74 | linux/386 75 | linux/ppc64le 76 | linux/s390x -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | .vertx 35 | out 36 | 37 | #/src/main/resources/application-prod.yml 38 | /src/main/resources/application-default.yml 39 | 40 | /src/main/resources/web 41 | /storage* 42 | /logs 43 | /bin 44 | /file-uploads 45 | /reader-assets 46 | /.lh/ 47 | 48 | /simple-web/ 49 | 50 | /server/logs 51 | /server/storage 52 | /server/target -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hectorqin/reader 2 | 3 | # 时区 4 | ENV TZ=Asia/Shanghai 5 | 6 | EXPOSE 8080 7 | ENTRYPOINT ["/sbin/tini", "--"] 8 | CMD ["java", "-jar", "/app/bin/reader.jar" ] 9 | -------------------------------------------------------------------------------- /Dockerfile.source: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine3.14 AS build-web 2 | ADD . /app 3 | WORKDIR /app/web 4 | # Build web 5 | RUN yarn && yarn build 6 | 7 | # Build jar 8 | FROM gradle:6.1.1-jdk8 AS build-env 9 | ADD --chown=gradle:gradle . /app 10 | WORKDIR /app 11 | COPY --from=build-web /app/web/dist /app/src/main/resources/web 12 | RUN \ 13 | rm src/main/java/com/htmake/reader/ReaderUIApplication.kt; \ 14 | gradle -b cli.gradle assemble --info; \ 15 | mv ./build/libs/*.jar ./build/libs/reader.jar 16 | 17 | FROM amazoncorretto:8u332-alpine3.14-jre 18 | # Install base packages 19 | RUN \ 20 | # apk update; \ 21 | # apk upgrade; \ 22 | # Add CA certs tini tzdata 23 | apk add --no-cache ca-certificates tini tzdata; \ 24 | update-ca-certificates; \ 25 | # Clean APK cache 26 | rm -rf /var/cache/apk/*; 27 | 28 | # 时区 29 | ENV TZ=Asia/Shanghai 30 | 31 | #RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 32 | # && echo Asia/Shanghai > /etc/timdezone \ 33 | # && dpkg-reconfigure -f noninteractive tzdata 34 | 35 | EXPOSE 8080 36 | ENTRYPOINT ["/sbin/tini", "--"] 37 | # COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthas 38 | COPY --from=build-env /app/build/libs/reader.jar /app/bin/reader.jar 39 | CMD ["java", "-jar", "/app/bin/reader.jar" ] 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reader 2 | 3 | 阅读3服务器版,不需要手机。 4 | 5 | 加入TG群(限时开放) 👉 [https://t.me/+pQ8HDlANPZ84ZWNl](https://t.me/+pQ8HDlANPZ84ZWNl) 6 | 7 | 关注公众号,查看教程和书源👇 8 | 9 | ![](imgs/mpcode.png) 10 | 11 | > 注意❗️ 12 | > 13 | > Reader 完整源码仅开放到 v2.5.4,新版本当前仅开放部分开源源码,见 https://github.com/hectorqin/reader-legado. 14 | 15 |
免责声明(Disclaimer) 16 | 阅读是一款提供网络文学搜索的工具,为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。 17 | 18 | 当您搜索一本书的时,阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。各第三方网站返回的内容与阅读无关,阅读对其概不负责,亦不承担任何法律责任。任何通过使用阅读而链接到的第三方网页均系他人制作或提供,您可能从第三方网页上获得其他服务,阅读对其合法性概不负责,亦不承担任何法律责任。第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读,不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。您应该对使用搜索引擎的结果自行承担风险。 19 | 20 | 阅读不做任何形式的保证:不保证第三方搜索引擎的搜索结果满足您的要求,不保证搜索服务不中断,不保证搜索结果的安全性、正确性、及时性、合法性。因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读,阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权,您注册的用户名、电子邮件地址等个人资料,非经您亲自许可或根据相关法律、法规的强制性规定,阅读不会主动地泄露给第三方。 21 | 22 | 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费,通过专业搜索展示不同网站中网络文学的最新章节。阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时,也使优秀网络文学得以迅速、更广泛的传播,从而达到了在一定程度促进网络文学充分繁荣发展之目的。阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商,并建议阅读正版图书。任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权,应该及时向阅读提出书面权力通知,并提供身份证明、权属证明及详细侵权情况证明。阅读在收到上述法律文件后,将会依法尽快断开相关链接内容。 23 |
24 | 25 |
功能说明 26 | 书源管理
27 | - 书架管理
28 | - 书架布局
29 | - 搜索
30 | - 书海
31 | - 看书
32 | - 移动端适配
33 | - 换源
34 | - 翻页方式
35 | - 手势支持
36 | - 自定义主题
37 | - 自定义样式
38 | - WebDAV同步
39 | - 文字替换过滤
40 | - 听书<仅部分浏览器支持,手机端会因为锁屏而失效>
41 | - 用户配置备份恢复
42 | - 支持漫画
43 | - 支持音频
44 | - 书源失效检测
45 | - 导入本地TXT、EPUB、UMD、PDF格式的书籍
46 | - 书籍分组
47 | - RSS订阅
48 | - 定时更新书架
49 | - 并发搜书
50 | - 本地书仓
51 | - 支持kindle阅读
52 |
53 | 54 | ## 下载与安装 55 | 56 | 详见[文档](https://github.com/hectorqin/reader/blob/master/doc.md) 57 | 58 | ## 问题 59 | 60 | - 部分使用了 `Javascript` 的书源可能会报错,如调用原生java等高级Javascript功能 61 | - `webview` 功能需要另外部署接口,不支持 `sourceRegex` 匹配资源响应 62 | - 不支持书源登录功能 63 | 64 | ## 感谢 65 | 66 | - 项目初期参考了 [lightink-小说API](https://github.com/lightink-qingmo/lightink-server) 67 | - [阅读](https://github.com/gedoor/MyBookshelf) 68 | - [阅读3.0](https://github.com/gedoor/legado) 69 | - 项目初期参考了 [阅读3.0Web端](https://github.com/celetor/web-yuedu3) 70 | 71 | ## 其它 72 | 73 | - [帮助文档](https://github.com/hectorqin/reader/blob/master/doc.md) 74 | - [界面预览](https://github.com/hectorqin/reader/blob/master/preview.md) 75 | -------------------------------------------------------------------------------- /assets/linux/reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/assets/linux/reader.png -------------------------------------------------------------------------------- /assets/mac/reader.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/assets/mac/reader.icns -------------------------------------------------------------------------------- /assets/windows/reader.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/assets/windows/reader.ico -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | # reader 在线阅读 4 | # 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周) 5 | # 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可 6 | # 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect) 7 | # 手动更新方式 : docker-compose pull && docker-compose up -d 8 | reader: 9 | image: hectorqin/reader 10 | #image: hectorqin/reader:openj9-latest #docker镜像,arm64架构或小内存机器优先使用此镜像.启用需删除上一行 11 | container_name: reader #容器名 可自行修改 12 | restart: always 13 | ports: 14 | - 4396:8080 #4396端口映射可自行修改 15 | networks: 16 | - share_net 17 | volumes: 18 | - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改 19 | - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改 20 | environment: 21 | - SPRING_PROFILES_ACTIVE=prod 22 | - READER_APP_USERLIMIT=50 #用户上限,默认50 23 | - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200 24 | - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 V2.0 25 | # 如果启用远程webview,需要取消注释下面的 remote-webview 服务 26 | # - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8050 #开启远程webview 27 | # 下面都是多用户模式配置 28 | - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式 29 | - READER_APP_SECUREKEY=adminpwd #管理员密码 建议修改 30 | - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除 31 | # remote-webview: 32 | # image: hectorqin/remote-webview 33 | # container_name: remote-webview #容器名 可自行修改 34 | # restart: always 35 | # ports: 36 | # - 8050:8050 37 | # networks: 38 | # - share_net 39 | # 自动更新docker镜像 40 | watchtower: 41 | image: containrrr/watchtower 42 | container_name: watchtower 43 | restart: always 44 | # 环境变量,设置为上海时区 45 | environment: 46 | - TZ=Asia/Shanghai 47 | volumes: 48 | - /var/run/docker.sock:/var/run/docker.sock 49 | command: reader watchtower --cleanup --schedule "0 0 4 * * *" 50 | networks: 51 | - share_net 52 | # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx 53 | # --cleanup 更新后清理旧版本镜像 54 | # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整 55 | networks: 56 | share_net: 57 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | # reader 在线阅读 4 | # 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周) 5 | # 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可 6 | # 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect) 7 | # 手动更新方式 : docker-compose pull && docker-compose up -d 8 | reader: 9 | #image: hectorqin/reader # 普通镜像 10 | image: hectorqin/reader:openj9-latest # Openj9镜像,arm64架构或小内存机器优先使用 11 | container_name: reader #容器名 可自行修改 12 | restart: always 13 | ports: 14 | - 4396:8080 #4396端口映射可自行修改,8080请勿修改 15 | volumes: 16 | - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改 17 | - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改 18 | environment: 19 | - SPRING_PROFILES_ACTIVE=prod 20 | #- READER_APP_USERLIMIT=50 #用户上限,默认且最大值为50 21 | - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200 22 | - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 23 | - READER_APP_REMOTEWEBVIEWAPI=http://readerwebview:8050 #启用webview(若下方readerwebview容器不开启需注释此行 24 | # ↓多用户模式配置↓ 25 | - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式 26 | - READER_APP_SECUREKEY=adminpwd #管理员密码 建议修改 27 | - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除 28 | # 如需支持webview书源,打开(占用较大,不需要可加 # 注释) 29 | readerwebview: 30 | image: hectorqin/remote-webview 31 | container_name: readerwebview 32 | restart: always 33 | environment: 34 | - TZ=Asia/Shanghai 35 | # 自动更新docker镜像 36 | watchtower: 37 | image: containrrr/watchtower 38 | container_name: watchtower 39 | restart: always 40 | environment: 41 | - TZ=Asia/Shanghai 42 | volumes: 43 | - /var/run/docker.sock:/var/run/docker.sock 44 | command: reader readerwebview watchtower --cleanup --schedule "0 0 4 * * *" 45 | # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx 46 | # --cleanup 更新后清理旧版本镜像 47 | # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整 48 | volumes: 49 | reader: 50 | readerwebview: -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /imgs/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/1.jpg -------------------------------------------------------------------------------- /imgs/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/10.jpg -------------------------------------------------------------------------------- /imgs/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/2.jpg -------------------------------------------------------------------------------- /imgs/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/3.jpg -------------------------------------------------------------------------------- /imgs/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/4.jpg -------------------------------------------------------------------------------- /imgs/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/5.jpg -------------------------------------------------------------------------------- /imgs/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/6.jpg -------------------------------------------------------------------------------- /imgs/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/7.jpg -------------------------------------------------------------------------------- /imgs/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/8.jpg -------------------------------------------------------------------------------- /imgs/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/9.jpg -------------------------------------------------------------------------------- /imgs/mpcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/imgs/mpcode.png -------------------------------------------------------------------------------- /nixpacks.toml: -------------------------------------------------------------------------------- 1 | [phases.build] 2 | cmds = ['railway run'] 3 | 4 | [start] 5 | runImage = 'hectorqin/reader' 6 | onlyIncludeFiles = ['Dockerfile'] 7 | cmd = '/sbin/tini --' -------------------------------------------------------------------------------- /preview.md: -------------------------------------------------------------------------------- 1 | # 预览 2 | 3 | ![](imgs/1.jpg) 4 | ![](imgs/2.jpg) 5 | ![](imgs/3.jpg) 6 | ![](imgs/4.jpg) 7 | ![](imgs/5.jpg) 8 | ![](imgs/6.jpg) 9 | ![](imgs/7.jpg) 10 | ![](imgs/8.jpg) 11 | ![](imgs/9.jpg) 12 | ![](imgs/10.jpg) -------------------------------------------------------------------------------- /server/bin/shutdown.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Copyright 1999-2018 Alibaba Group Holding Ltd. 3 | rem Licensed under the Apache License, Version 2.0 (the "License"); 4 | rem you may not use this file except in compliance with the License. 5 | rem You may obtain a copy of the License at 6 | rem 7 | rem http://www.apache.org/licenses/LICENSE-2.0 8 | rem 9 | rem Unless required by applicable law or agreed to in writing, software 10 | rem distributed under the License is distributed on an "AS IS" BASIS, 11 | rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | rem See the License for the specific language governing permissions and 13 | rem limitations under the License. 14 | 15 | if not exist "%JAVA_HOME%\bin\java.exe" ( 16 | rem find java_home from reg 17 | for /f "tokens=2*" %%i in ('reg query "HKLM\SOFTWARE\JavaSoft\Java Runtime Environment" /s ^| findstr "JavaHome"') do ( 18 | set "JAVA_HOME=%%j" 19 | ) 20 | ) 21 | if exist "%JAVA_HOME%\bin\java.exe" ( 22 | set "JAVA=%JAVA_HOME%\bin\java.exe" 23 | ) else ( 24 | echo Please set the JAVA_HOME variable in your environment, We need jdk8 or later! 25 | pause 26 | EXIT /B 1 27 | ) 28 | 29 | setlocal 30 | 31 | set "PATH=%JAVA_HOME%\bin;%PATH%" 32 | 33 | echo killing reader server 34 | 35 | for /f "tokens=1" %%i in ('jps -m ^| find "reader.server"') do ( taskkill /F /PID %%i ) 36 | 37 | echo Done! 38 | -------------------------------------------------------------------------------- /server/bin/shutdown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 1999-2018 Alibaba Group Holding Ltd. 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | cd `dirname $0`/../target 16 | target_dir=`pwd` 17 | 18 | pid=`ps ax | grep -i 'reader.server' | grep ${target_dir} | grep java | grep -v grep | awk '{print $1}'` 19 | if [ -z "$pid" ] ; then 20 | echo "No reader running." 21 | exit -1; 22 | fi 23 | 24 | echo "The reader(${pid}) is running..." 25 | 26 | kill ${pid} 27 | 28 | echo "Send shutdown request to reader(${pid}) OK" 29 | -------------------------------------------------------------------------------- /server/conf/application.properties: -------------------------------------------------------------------------------- 1 | # 是否多用户模式,如果启动 startup 脚本时使用了 -m 1 选择多用户模式,-m single 运行单用户模式,否则根据此处的参数选择模式 2 | reader.app.secure=true 3 | 4 | # 邀请码,如果启动 startup 脚本时使用了参数 -i 邀请码,则会覆盖此处 5 | reader.app.inviteCode= 6 | 7 | # 管理密码,如果启动 startup 脚本时使用了参数 -k 管理密码,则会覆盖此处 8 | reader.app.secureKey= 9 | 10 | # 书源代理可通过 header 设置 11 | 12 | # 是否缓存章节内容 13 | reader.app.cacheChapterContent=true 14 | 15 | # 用户上限,免费版用户上限最大15 16 | reader.app.userLimit=15 17 | 18 | # 是否开启书源调试日志 19 | reader.app.debugLog=false 20 | 21 | # 自动清理不活跃用户,单位天,0为不清理,大于0的数字为清理多少天未登录用户 22 | reader.app.autoClearInactiveUser=0 23 | 24 | # mongodb数据备份,mongodb链接地址 25 | reader.app.mongoUri= 26 | # mongodb数据备份,mongodb数据库名 27 | reader.app.mongoDbName=reader 28 | 29 | # 书架自动更新间隔,单位分钟 30 | reader.app.shelfUpdateInteval=10 31 | 32 | # 远程webview接口地址,可通过部署 hectorqin/remote-webview 来设置,http://IP:8050 33 | reader.app.remoteWebviewApi= 34 | 35 | # 新用户默认是否启用 webdav 36 | reader.app.defaultUserEnableWebdav=true 37 | # 新用户是否默认启用 本地书仓 38 | reader.app.defaultUserEnableLocalStore=true 39 | # 新用户是否默认启用 书源编辑 40 | reader.app.defaultUserEnableBookSource=true 41 | # 新用户是否默认启用 RSS源编辑 42 | reader.app.defaultUserEnableRssSource=true 43 | # 新用户是否默认书源数量上限 44 | reader.app.defaultUserBookSourceLimit=100 45 | # 新用户是否默认书籍数量上限 46 | reader.app.defaultUserBookLimit=200 47 | 48 | # 是否自动备份用户数据 49 | reader.app.autoBackupUserData=false 50 | 51 | # reader服务监听端口 52 | reader.server.port=8080 53 | # reader接口目录 54 | reader.server.contextPath= -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | rootProject.name = 'reader' 7 | -------------------------------------------------------------------------------- /src/lib/rhino-1.7.13-1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/lib/rhino-1.7.13-1.jar -------------------------------------------------------------------------------- /src/lib/xmlpull-1.1.3.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/lib/xmlpull-1.1.3.1.jar -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/ReaderApplication.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule 5 | import io.vertx.core.Future 6 | import io.vertx.core.Vertx 7 | import io.vertx.core.http.* 8 | import io.vertx.core.json.Json 9 | import io.vertx.ext.web.client.WebClient 10 | import io.vertx.ext.web.client.WebClientOptions 11 | import mu.KotlinLogging 12 | import com.htmake.reader.api.YueduApi 13 | 14 | import com.htmake.reader.verticle.RestVerticle 15 | import org.springframework.beans.factory.annotation.Autowired 16 | import org.springframework.boot.SpringApplication 17 | import org.springframework.boot.autoconfigure.SpringBootApplication 18 | import org.springframework.scheduling.annotation.EnableScheduling; 19 | import org.springframework.context.annotation.Bean 20 | import javax.annotation.PostConstruct 21 | 22 | private val logger = KotlinLogging.logger {} 23 | 24 | @SpringBootApplication 25 | @EnableScheduling 26 | class ReaderApplication { 27 | 28 | @Autowired 29 | private lateinit var yueduApi: YueduApi 30 | 31 | companion object { 32 | val vertx by lazy { Vertx.vertx() } 33 | fun vertx() = vertx 34 | } 35 | 36 | @PostConstruct 37 | fun deployVerticle() { 38 | Json.mapper.apply { 39 | registerKotlinModule() 40 | } 41 | 42 | Json.prettyMapper.apply { 43 | registerKotlinModule() 44 | } 45 | 46 | Json.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 47 | 48 | vertx().deployVerticle(yueduApi) 49 | } 50 | 51 | @Bean 52 | fun webClient(): WebClient { 53 | val webClientOptions = WebClientOptions() 54 | webClientOptions.isTryUseCompression = true 55 | webClientOptions.logActivity = true 56 | webClientOptions.isFollowRedirects = true 57 | webClientOptions.isTrustAll = true 58 | 59 | val httpClient = vertx().createHttpClient(HttpClientOptions().setTrustAll(true)) 60 | 61 | // val webClient = WebClient.wrap(HttpClient(delegateHttpClient), webClientOptions) 62 | val webClient = WebClient.wrap(httpClient, webClientOptions) 63 | 64 | return webClient 65 | } 66 | 67 | } 68 | 69 | fun main(args: Array) { 70 | SpringApplication.run(ReaderApplication::class.java, *args) 71 | } 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/SpringEvent.java: -------------------------------------------------------------------------------- 1 | package com.htmake.reader; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class SpringEvent extends ApplicationEvent { 6 | private String event; 7 | private String message; 8 | public SpringEvent(Object source, String event, String message) { 9 | super(source); 10 | this.event = event; 11 | this.message = message; 12 | } 13 | public String getEvent() { 14 | return event; 15 | } 16 | public void setEvent(String event) { 17 | this.event = event; 18 | } 19 | public String getMessage() { 20 | return message; 21 | } 22 | public void setMessage(String message) { 23 | this.message = message; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/api/ReturnData.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.api 2 | 3 | 4 | class ReturnData { 5 | 6 | var isSuccess: Boolean = false 7 | private set 8 | 9 | var errorMsg: String = "未知错误,请联系开发者!" 10 | private set 11 | 12 | var data: Any? = null 13 | private set 14 | 15 | fun setErrorMsg(errorMsg: String): ReturnData { 16 | this.isSuccess = false 17 | this.errorMsg = errorMsg 18 | return this 19 | } 20 | 21 | fun setData(data: Any): ReturnData { 22 | this.isSuccess = true 23 | this.errorMsg = "" 24 | this.data = data 25 | return this 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/config/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.config 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | @ConfigurationProperties(prefix = "reader.app") 8 | class AppConfig { 9 | lateinit var storagePath: String // 存储路径 10 | var showUI = false // 是否显示UI 11 | var debug = false // 是否调试web 12 | var packaged = false // 是否打包为app 13 | var secure = false // 是否启用登录鉴权 14 | var inviteCode = "" // 注册邀请码 15 | var secureKey = "" // 管理密码 16 | var cacheChapterContent = false // 是否缓存章节内容 17 | var userLimit = 50 // 用户上限 18 | var userBookLimit = 200 // 用户书籍上限 19 | var debugLog = false // 调试日志 20 | var autoClearInactiveUser = 0 // 自动清理不活跃用户 21 | 22 | var exportUseReplace = false // 导出不使用净化 23 | var exportCharset = "UTF-8" // 导出字符集 24 | var exportNoChapterName = false // 不添加章节名 25 | var exportPictureFile = false // 导出图片 26 | } -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/entity/BasicError.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.entity 2 | 3 | data class BasicError( 4 | val error: String, 5 | val exception: String, 6 | val message: String, 7 | val path: String, 8 | val status: Int, 9 | val timestamp: Long 10 | ) -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/entity/Size.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.entity 2 | 3 | data class Size( 4 | val width: Double, 5 | val height: Double 6 | ) -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/entity/User.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.entity 2 | 3 | data class User( 4 | var username: String="", 5 | var password: String="", 6 | var salt: String="", 7 | var token: String="", 8 | var last_login_at: Long = System.currentTimeMillis(), 9 | var created_at: Long = System.currentTimeMillis(), 10 | var enable_webdav: Boolean = false, // 是否开启 WebDAV 功能 11 | var token_map: Map? = null, 12 | var enable_local_store: Boolean = false // 是否开启本地书仓功能 13 | ) -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/init/appCtx.kt: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.init 2 | 3 | import com.htmake.reader.utils.getWorkDir 4 | 5 | // 处理 appCtx 6 | object appCtx { 7 | val cacheDir: String by lazy { 8 | getWorkDir("storage", "cache") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/htmake/reader/utils/SpringContextUtils.java: -------------------------------------------------------------------------------- 1 | package com.htmake.reader.utils; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class SpringContextUtils implements ApplicationContextAware { 10 | 11 | /** 12 | * 上下文对象实例 13 | */ 14 | private static ApplicationContext applicationContext; 15 | 16 | @Override 17 | public void setApplicationContext(ApplicationContext context) throws BeansException { 18 | applicationContext = context; 19 | } 20 | 21 | /** 22 | * 获取applicationContext 23 | * 24 | * @return 25 | */ 26 | public static ApplicationContext getApplicationContext() { 27 | return applicationContext; 28 | } 29 | 30 | /** 31 | * 通过name获取 Bean. 32 | * 33 | * @param name 34 | * @return 35 | */ 36 | public static Object getBean(String name) { 37 | if (applicationContext != null) { 38 | return getApplicationContext().getBean(name); 39 | } 40 | return null; 41 | } 42 | 43 | /** 44 | * 通过class获取Bean. 45 | * 46 | * @param clazz 47 | * @param 48 | * @return 49 | */ 50 | public static T getBean(Class clazz) { 51 | if (applicationContext != null) { 52 | return getApplicationContext().getBean(clazz); 53 | } 54 | return null; 55 | } 56 | 57 | /** 58 | * 通过name,以及Clazz返回指定的Bean 59 | * 60 | * @param name 61 | * @param clazz 62 | * @param 63 | * @return 64 | */ 65 | public static T getBean(String name, Class clazz) { 66 | if (applicationContext != null) { 67 | return getApplicationContext().getBean(name, clazz); 68 | } 69 | return null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/README.md: -------------------------------------------------------------------------------- 1 | # 文件结构介绍 2 | 3 | * constant 常量 4 | * data 数据 5 | * help 帮助 6 | * lib 库 7 | * model 解析 8 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/Action.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | object Action { 4 | 5 | const val play = "play" 6 | const val stop = "stop" 7 | const val resume = "resume" 8 | const val pause = "pause" 9 | const val addTimer = "addTimer" 10 | const val setTimer = "setTimer" 11 | const val prevParagraph = "prevParagraph" 12 | const val nextParagraph = "nextParagraph" 13 | const val upTtsSpeechRate = "upTtsSpeechRate" 14 | const val adjustProgress = "adjustProgress" 15 | const val prev = "prev" 16 | const val next = "next" 17 | const val moveTo = "moveTo" 18 | const val init = "init" 19 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/AppConst.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | import java.text.SimpleDateFormat 4 | import com.script.javascript.RhinoScriptEngine 5 | 6 | object AppConst { 7 | 8 | 9 | const val UA_NAME = "User-Agent" 10 | 11 | val userAgent: String by lazy { 12 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" 13 | } 14 | 15 | val SCRIPT_ENGINE: RhinoScriptEngine by lazy { 16 | RhinoScriptEngine() 17 | } 18 | 19 | val TIME_FORMAT: SimpleDateFormat by lazy { 20 | SimpleDateFormat("HH:mm") 21 | } 22 | 23 | val timeFormat: SimpleDateFormat by lazy { 24 | SimpleDateFormat("HH:mm") 25 | } 26 | 27 | val dateFormat: SimpleDateFormat by lazy { 28 | SimpleDateFormat("yyyy/MM/dd HH:mm") 29 | } 30 | 31 | val fileNameFormat: SimpleDateFormat by lazy { 32 | SimpleDateFormat("yy-MM-dd-HH-mm-ss") 33 | } 34 | 35 | val keyboardToolChars: List by lazy { 36 | arrayListOf( 37 | "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".", 38 | "href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag" 39 | ) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/AppPattern.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | import java.util.regex.Pattern 4 | 5 | object AppPattern { 6 | val JS_PATTERN: Pattern = 7 | Pattern.compile("([\\w\\W]*?)|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE) 8 | val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") 9 | 10 | //匹配格式化后的图片格式 11 | val imgPattern: Pattern = Pattern.compile("]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>") 12 | 13 | //dataURL图片类型 14 | val dataUriRegex = Regex("data:.*?;base64,(.*)") 15 | 16 | val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著") 17 | val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著") 18 | val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") 19 | val splitGroupRegex = Regex("[,;,;]") 20 | 21 | //书源调试信息中的各种符号 22 | val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]") 23 | 24 | //本地书籍支持类型 25 | val bookFileRegex = Regex(".*\\.(txt|epub|umd)", RegexOption.IGNORE_CASE) 26 | 27 | /** 28 | * 所有标点 29 | */ 30 | val bdRegex = Regex("(\\p{P})+") 31 | 32 | /** 33 | * 换行 34 | */ 35 | val rnRegex = Regex("[\\r\\n]") 36 | 37 | /** 38 | * 不发音段落判断 39 | */ 40 | val notReadAloudRegex = Regex("^(\\s|\\p{C}|\\p{P}|\\p{Z}|\\p{S})+$") 41 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/BookType.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | object BookType { 4 | const val default = 0 // 0 文本 5 | const val audio = 1 // 1 音频 6 | const val image = 2 // 2 图片 7 | const val file = 3 // 3 只提供下载服务的网站 8 | const val local = "loc_book" 9 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/DeepinkBookSource.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | import java.io.File 4 | 5 | object DeepinkBookSource { 6 | 7 | fun generate(name: String, url: String, md5: String) { 8 | val text = "{\n" + 9 | " \"name\": \"$name by [yuedu.best]\",\n" + 10 | " \"url\": \"$url\",\n" + 11 | " \"version\": 100,\n" + 12 | " \"search\": {\n" + 13 | " \"url\": \"http://api.yuedu.best/yuedu/searchBook@post->{\\\"key\\\":\\\"\${key}\\\", \\\"bookSourceCode\\\":\\\"$md5\\\"}\",\n" + 14 | " \"charset\": \"utf-8\",\n" + 15 | " \"list\": \"\$.[*]\",\n" + 16 | " \"name\": \"\$.name\",\n" + 17 | " \"author\": \"\$.author\",\n" + 18 | " \"cover\": \"\$.coverUrl\",\n" + 19 | " \"summary\": \"\$.intro\",\n" + 20 | " \"detail\": \"http://api.yuedu.best/yuedu/getBookInfo@post->{\\\"searchBook\\\":\${$}, \\\"bookSourceCode\\\":\\\"$md5\\\"}\"\n" + 21 | " },\n" + 22 | " \"detail\": {\n" + 23 | " \"name\": \"\$.name\",\n" + 24 | " \"author\": \"\$.author\",\n" + 25 | " \"cover\": \"\$.coverUrl\",\n" + 26 | " \"summary\": \"\$.intro\",\n" + 27 | " \"status\": \"\",\n" + 28 | " \"update\": \"\$.latestChapterTime\",\n" + 29 | " \"lastChapter\": \"\$.latestChapterTitle\",\n" + 30 | " \"catalog\": \"http://api.yuedu.best/yuedu/getChapterList@post->{\\\"book\\\":\${$}, \\\"bookSourceCode\\\":\\\"$md5\\\"}\"\n" + 31 | " },\n" + 32 | " \"catalog\": {\n" + 33 | " \"list\": \"\$.[*]\",\n" + 34 | " \"name\": \"\$.title\",\n" + 35 | " \"chapter\": \"http://api.yuedu.best/yuedu/getContent@post->{\\\"bookChapter\\\":\${$}, \\\"bookSourceCode\\\":\\\"$md5\\\"}\"\n" + 36 | " },\n" + 37 | " \"chapter\": {\n" + 38 | " \"content\": \"\$.text\"\n" + 39 | " }\n" + 40 | "}" 41 | 42 | val file = File("repo/${url.replace("https://","").replace("http://","")}.json") 43 | println("file path: "+ file.absoluteFile) 44 | file.createNewFile() 45 | file.writeText(text) 46 | // println("file path: "+ file.absoluteFile) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/PreferKey.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | object PreferKey { 4 | 5 | const val downloadPath = "downloadPath" 6 | const val hideStatusBar = "hideStatusBar" 7 | const val hideNavigationBar = "hideNavigationBar" 8 | const val precisionSearch = "precisionSearch" 9 | const val prevKey = "prevKeyCode" 10 | const val nextKey = "nextKeyCode" 11 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/RSSKeywords.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | object RSSKeywords { 4 | 5 | const val RSS_ITEM = "item" 6 | const val RSS_ITEM_TITLE = "title" 7 | const val RSS_ITEM_LINK = "link" 8 | const val RSS_ITEM_CATEGORY = "category" 9 | const val RSS_ITEM_THUMBNAIL = "media:thumbnail" 10 | const val RSS_ITEM_ENCLOSURE = "enclosure" 11 | const val RSS_ITEM_DESCRIPTION = "description" 12 | const val RSS_ITEM_CONTENT = "content:encoded" 13 | const val RSS_ITEM_PUB_DATE = "pubDate" 14 | const val RSS_ITEM_TIME = "time" 15 | const val RSS_ITEM_URL = "url" 16 | const val RSS_ITEM_TYPE = "type" 17 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/constant/Status.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.constant 2 | 3 | object Status { 4 | const val STOP = 0 5 | const val PLAY = 1 6 | const val PAUSE = 3 7 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/BaseBook.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | import io.legado.app.model.analyzeRule.RuleDataInterface 4 | import io.legado.app.utils.splitNotBlank 5 | 6 | interface BaseBook : RuleDataInterface { 7 | var name: String 8 | var author: String 9 | var bookUrl: String 10 | var kind: String? 11 | var wordCount: String? 12 | 13 | var infoHtml: String? 14 | var tocHtml: String? 15 | 16 | fun getKindList(): List { 17 | val kindList = arrayListOf() 18 | wordCount?.let { 19 | if (it.isNotBlank()) kindList.add(it) 20 | } 21 | kind?.let { 22 | val kinds = it.splitNotBlank(",", "\n") 23 | kindList.addAll(kinds) 24 | } 25 | return kindList 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/BookChapter.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | 4 | import io.legado.app.utils.GSON 5 | import io.legado.app.utils.fromJsonObject 6 | import io.legado.app.utils.MD5Utils 7 | import io.legado.app.model.analyzeRule.AnalyzeUrl 8 | import io.legado.app.model.analyzeRule.RuleDataInterface 9 | import io.legado.app.utils.NetworkUtils 10 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties 11 | 12 | @JsonIgnoreProperties("variableMap") 13 | data class BookChapter( 14 | var url: String = "", // 章节地址 15 | var title: String = "", // 章节标题 16 | var isVolume: Boolean = false, // 是否是卷名 17 | var baseUrl: String = "", //用来拼接相对url 18 | var bookUrl: String = "", // 书籍地址 19 | var index: Int = 0, // 章节序号 20 | var resourceUrl: String? = null, // 音频真实URL 21 | var tag: String? = null, // 22 | var start: Long? = null, // 章节起始位置 23 | var end: Long? = null, // 章节终止位置 24 | var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId 25 | var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId 26 | var variable: String? = null //变量 27 | ): RuleDataInterface { 28 | 29 | @delegate:Transient 30 | override val variableMap: HashMap by lazy { 31 | GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() 32 | } 33 | 34 | override fun putVariable(key: String, value: String?) { 35 | if (value != null) { 36 | variableMap[key] = value 37 | } else { 38 | variableMap.remove(key) 39 | } 40 | variable = GSON.toJson(variableMap) 41 | } 42 | 43 | override fun hashCode() = url.hashCode() 44 | 45 | override fun equals(other: Any?): Boolean { 46 | if (other is BookChapter) { 47 | return other.url == url 48 | } 49 | return false 50 | } 51 | 52 | fun getAbsoluteURL():String{ 53 | val urlMatcher = AnalyzeUrl.paramPattern.matcher(url) 54 | val urlBefore = if(urlMatcher.find())url.substring(0,urlMatcher.start()) else url 55 | val urlAbsoluteBefore = NetworkUtils.getAbsoluteURL(baseUrl,urlBefore) 56 | return if(urlBefore.length == url.length) urlAbsoluteBefore else urlAbsoluteBefore + ',' + url.substring(urlMatcher.end()) 57 | } 58 | 59 | 60 | fun getFileName(): String = String.format("%05d-%s.nb", index, MD5Utils.md5Encode16(title)) 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/BookGroup.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | 4 | 5 | 6 | //@Parcelize 7 | //@Entity(tableName = "book_groups") 8 | data class BookGroup( 9 | // @PrimaryKey 10 | var groupId: Int = 0, 11 | var groupName: String = "", 12 | var order: Int = 0, 13 | var show: Boolean = true 14 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/Bookmark.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | 4 | 5 | 6 | //@Parcelize 7 | //@Entity(tableName = "bookmarks", indices = [(Index(value = ["bookUrl"], unique = true))]) 8 | data class Bookmark( 9 | // @PrimaryKey 10 | val time: Long = System.currentTimeMillis(), 11 | val bookName: String = "", 12 | val bookAuthor: String = "", 13 | var chapterIndex: Int = 0, 14 | var chapterPos: Int = 0, 15 | var chapterName: String = "", 16 | var bookText: String = "", 17 | var content: String = "" 18 | 19 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/Cache.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | // @Entity(tableName = "caches", indices = [(Index(value = ["key"], unique = true))]) 4 | data class Cache( 5 | // @PrimaryKey 6 | val key: String = "", 7 | var value: String? = null, 8 | var deadline: Long = 0L 9 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/Cookie.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | // @Entity(tableName = "cookies", indices = [(Index(value = ["url"], unique = true))]) 4 | data class Cookie( 5 | // @PrimaryKey 6 | var url: String = "", 7 | var cookie: String = "" 8 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/ReplaceRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | 6 | 7 | //@Parcelize 8 | //@Entity( 9 | // tableName = "replace_rules", 10 | // indices = [(Index(value = ["id"]))] 11 | //) 12 | data class ReplaceRule( 13 | // @PrimaryKey(autoGenerate = true) 14 | var id: Long = System.currentTimeMillis(), 15 | var name: String = "", 16 | var group: String? = null, 17 | var pattern: String = "", 18 | var replacement: String = "", 19 | var scope: String? = null, 20 | @get:JsonProperty("isEnabled") var isEnabled: Boolean = true, 21 | @get:JsonProperty("isRegex") var isRegex: Boolean = false, 22 | // @ColumnInfo(name = "sortOrder") 23 | var order: Int = 0 24 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/RssArticle.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | import io.legado.app.utils.GSON 4 | import io.legado.app.utils.fromJsonObject 5 | import io.legado.app.model.analyzeRule.RuleDataInterface 6 | 7 | data class RssArticle( 8 | var origin: String = "", 9 | var sort: String = "", 10 | var title: String = "", 11 | var order: Long = 0, 12 | var link: String = "", 13 | var pubDate: String? = null, 14 | var description: String? = null, 15 | var content: String? = null, 16 | var image: String? = null, 17 | var read: Boolean = false, 18 | var variable: String? = null 19 | ): RuleDataInterface { 20 | 21 | override fun hashCode() = link.hashCode() 22 | 23 | override fun equals(other: Any?): Boolean { 24 | other ?: return false 25 | return if (other is RssArticle) origin == other.origin && link == other.link else false 26 | } 27 | 28 | @delegate:Transient 29 | override val variableMap: HashMap by lazy { 30 | GSON.fromJsonObject>(variable).getOrNull() ?: hashMapOf() 31 | } 32 | 33 | override fun putVariable(key: String, value: String?) { 34 | if (value != null) { 35 | variableMap[key] = value 36 | } else { 37 | variableMap.remove(key) 38 | } 39 | variable = GSON.toJson(variableMap) 40 | } 41 | 42 | // fun toStar() = RssStar( 43 | // origin = origin, 44 | // sort = sort, 45 | // title = title, 46 | // starTime = System.currentTimeMillis(), 47 | // link = link, 48 | // pubDate = pubDate, 49 | // description = description, 50 | // content = content, 51 | // image = image 52 | // ) 53 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/SearchKeyword.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | 4 | 5 | 6 | 7 | //@Parcelize 8 | //@Entity(tableName = "search_keywords", indices = [(Index(value = ["word"], unique = true))]) 9 | data class SearchKeyword( 10 | // @PrimaryKey 11 | var word: String = "", // 搜索关键词 12 | var usage: Int = 1, // 使用次数 13 | var lastUseTime: Long = System.currentTimeMillis() // 最后一次使用时间 14 | ) 15 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/SearchResult.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | data class SearchResult( 4 | val resultCount: Int = 0, 5 | val resultCountWithinChapter: Int = 0, 6 | val resultText: String = "", 7 | val chapterTitle: String = "", 8 | val query: String = "", 9 | val pageSize: Int = 0, 10 | val chapterIndex: Int = 0, 11 | val pageIndex: Int = 0, 12 | val queryIndexInResult: Int = 0, 13 | val queryIndexInChapter: Int = 0 14 | ) { 15 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/TxtTocRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities 2 | 3 | // import androidx.room.Entity 4 | // import androidx.room.PrimaryKey 5 | 6 | 7 | // @Entity(tableName = "txtTocRules") 8 | data class TxtTocRule( 9 | // @PrimaryKey 10 | var id: Long = System.currentTimeMillis(), 11 | var name: String = "", 12 | var rule: String = "", 13 | var serialNumber: Int = -1, 14 | var enable: Boolean = true 15 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities.rule 2 | 3 | data class BookInfoRule( 4 | var init: String? = null, 5 | var name: String? = null, 6 | var author: String? = null, 7 | var intro: String? = null, 8 | var kind: String? = null, 9 | var lastChapter: String? = null, 10 | var updateTime: String? = null, 11 | var coverUrl: String? = null, 12 | var tocUrl: String? = null, 13 | var wordCount: String? = null, 14 | var canReName: String? = null 15 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/rule/BookListRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities.rule 2 | 3 | interface BookListRule { 4 | var bookList: String? 5 | var name: String? 6 | var author: String? 7 | var intro: String? 8 | var kind: String? 9 | var lastChapter: String? 10 | var updateTime: String? 11 | var bookUrl: String? 12 | var coverUrl: String? 13 | var wordCount: String? 14 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/rule/ContentRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities.rule 2 | 3 | data class ContentRule( 4 | var content: String? = null, 5 | var nextContentUrl: String? = null, 6 | var webJs: String? = null, 7 | var sourceRegex: String? = null, 8 | var replaceRegex: String? = null, //替换规则 9 | var imageStyle: String? = null, //默认大小居中,FULL最大宽度 10 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities.rule 2 | 3 | data class ExploreRule( 4 | override var bookList: String? = null, 5 | override var name: String? = null, 6 | override var author: String? = null, 7 | override var intro: String? = null, 8 | override var kind: String? = null, 9 | override var lastChapter: String? = null, 10 | override var updateTime: String? = null, 11 | override var bookUrl: String? = null, 12 | override var coverUrl: String? = null, 13 | override var wordCount: String? = null 14 | ) : BookListRule -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/rule/SearchRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities.rule 2 | 3 | data class SearchRule( 4 | override var bookList: String? = null, 5 | override var name: String? = null, 6 | override var author: String? = null, 7 | override var intro: String? = null, 8 | override var kind: String? = null, 9 | override var lastChapter: String? = null, 10 | override var updateTime: String? = null, 11 | override var bookUrl: String? = null, 12 | override var coverUrl: String? = null, 13 | override var wordCount: String? = null 14 | ) : BookListRule -------------------------------------------------------------------------------- /src/main/java/io/legado/app/data/entities/rule/TocRule.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.data.entities.rule 2 | 3 | data class TocRule( 4 | var preUpdateJs: String? = null, 5 | var chapterList: String? = null, 6 | var chapterName: String? = null, 7 | var chapterUrl: String? = null, 8 | var isVolume: String? = null, 9 | var isVip: String? = null, 10 | var updateTime: String? = null, 11 | var nextTocUrl: String? = null 12 | ) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/exception/ConcurrentException.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legado.app.exception 4 | 5 | /** 6 | * 并发限制 7 | */ 8 | class ConcurrentException(msg: String, val waitTime: Int) : NoStackTraceException(msg) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/exception/ContentEmptyException.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.exception 2 | 3 | /** 4 | * 内容为空 5 | */ 6 | class ContentEmptyException(msg: String) : NoStackTraceException(msg) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/exception/NoStackTraceException.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.exception 2 | 3 | /** 4 | * 不记录错误堆栈的报错 5 | */ 6 | open class NoStackTraceException(msg: String) : Exception(msg) { 7 | 8 | override fun fillInStackTrace(): Throwable { 9 | return this 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/exception/RegexTimeoutException.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.exception 2 | 3 | class RegexTimeoutException(msg: String) : NoStackTraceException(msg) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/exception/TocEmptyException.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.exception 2 | 3 | /** 4 | * 目录为空 5 | */ 6 | class TocEmptyException(msg: String) : NoStackTraceException(msg) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/CacheManager.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help 2 | 3 | import io.legado.app.data.entities.Cache 4 | import io.legado.app.model.analyzeRule.QueryTTF 5 | import io.legado.app.utils.ACache 6 | 7 | // TODO 处理缓存 8 | @Suppress("unused") 9 | object CacheManager { 10 | 11 | private val queryTTFMap = hashMapOf>() 12 | 13 | /** 14 | * saveTime 单位为秒 15 | */ 16 | @JvmOverloads 17 | fun put(key: String, value: Any, saveTime: Int = 0) { 18 | val deadline = 19 | if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000 20 | when (value) { 21 | is QueryTTF -> queryTTFMap[key] = Pair(deadline, value) 22 | is ByteArray -> ACache.get().put(key, value, saveTime) 23 | else -> { 24 | val cache = Cache(key, value.toString(), deadline) 25 | // appDb.cacheDao.insert(cache) 26 | } 27 | } 28 | } 29 | 30 | fun get(key: String): String? { 31 | // return appDb.cacheDao.get(key, System.currentTimeMillis()) 32 | return null 33 | } 34 | 35 | fun getInt(key: String): Int? { 36 | return get(key)?.toIntOrNull() 37 | } 38 | 39 | fun getLong(key: String): Long? { 40 | return get(key)?.toLongOrNull() 41 | } 42 | 43 | fun getDouble(key: String): Double? { 44 | return get(key)?.toDoubleOrNull() 45 | } 46 | 47 | fun getFloat(key: String): Float? { 48 | return get(key)?.toFloatOrNull() 49 | } 50 | 51 | fun getByteArray(key: String): ByteArray? { 52 | return ACache.get().getAsBinary(key) 53 | } 54 | 55 | fun getQueryTTF(key: String): QueryTTF? { 56 | val cache = queryTTFMap[key] ?: return null 57 | if (cache.first == 0L || cache.first > System.currentTimeMillis()) { 58 | return cache.second 59 | } 60 | return null 61 | } 62 | 63 | fun putFile(key: String, value: String, saveTime: Int = 0) { 64 | ACache.get().put(key, value, saveTime) 65 | } 66 | 67 | fun getFile(key: String): String? { 68 | return ACache.get().getAsString(key) 69 | } 70 | 71 | fun delete(key: String) { 72 | ACache.get().remove(key) 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/DefaultData.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help 2 | 3 | // import io.legado.app.data.entities.RssSource 4 | import io.legado.app.data.entities.TxtTocRule 5 | import io.legado.app.utils.GSON 6 | import io.legado.app.utils.fromJsonArray 7 | import java.io.File 8 | 9 | object DefaultData { 10 | const val txtTocRuleFileName = "txtTocRule.json" 11 | 12 | val txtTocRules: List by lazy { 13 | val json = String(DefaultData::class.java.getResource("/defaultData/${txtTocRuleFileName}").readBytes()) 14 | GSON.fromJsonArray(json).getOrNull() ?: emptyList() 15 | } 16 | 17 | // val rssSources by lazy { 18 | // val json = String( 19 | // File("defaultData${File.separator}rssSources.json") 20 | // .readBytes() 21 | // ) 22 | // GSON.fromJsonArray(json)!! 23 | // } 24 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.coroutine 2 | 3 | class CompositeCoroutine : CoroutineContainer { 4 | 5 | private var resources: HashSet>? = null 6 | 7 | val size: Int 8 | get() = resources?.size ?: 0 9 | 10 | val isEmpty: Boolean 11 | get() = size == 0 12 | 13 | constructor() 14 | 15 | constructor(vararg coroutines: Coroutine<*>) { 16 | this.resources = hashSetOf(*coroutines) 17 | } 18 | 19 | constructor(coroutines: Iterable>) { 20 | this.resources = hashSetOf() 21 | for (d in coroutines) { 22 | this.resources?.add(d) 23 | } 24 | } 25 | 26 | override fun add(coroutine: Coroutine<*>): Boolean { 27 | synchronized(this) { 28 | var set: HashSet>? = resources 29 | if (resources == null) { 30 | set = hashSetOf() 31 | resources = set 32 | } 33 | return set!!.add(coroutine) 34 | } 35 | } 36 | 37 | override fun addAll(vararg coroutines: Coroutine<*>): Boolean { 38 | synchronized(this) { 39 | var set: HashSet>? = resources 40 | if (resources == null) { 41 | set = hashSetOf() 42 | resources = set 43 | } 44 | for (coroutine in coroutines) { 45 | val add = set!!.add(coroutine) 46 | if (!add) { 47 | return false 48 | } 49 | } 50 | } 51 | return true 52 | } 53 | 54 | override fun remove(coroutine: Coroutine<*>): Boolean { 55 | if (delete(coroutine)) { 56 | coroutine.cancel() 57 | return true 58 | } 59 | return false 60 | } 61 | 62 | override fun delete(coroutine: Coroutine<*>): Boolean { 63 | synchronized(this) { 64 | val set = resources 65 | if (set == null || !set.remove(coroutine)) { 66 | return false 67 | } 68 | } 69 | return true 70 | } 71 | 72 | override fun clear() { 73 | val set: HashSet>? 74 | synchronized(this) { 75 | set = resources 76 | resources = null 77 | } 78 | 79 | set?.forEachIndexed { _, coroutine -> 80 | coroutine.cancel() 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/coroutine/CoroutineContainer.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.coroutine 2 | 3 | internal interface CoroutineContainer { 4 | 5 | fun add(coroutine: Coroutine<*>): Boolean 6 | 7 | fun addAll(vararg coroutines: Coroutine<*>): Boolean 8 | 9 | fun remove(coroutine: Coroutine<*>): Boolean 10 | 11 | fun delete(coroutine: Coroutine<*>): Boolean 12 | 13 | fun clear() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/http/ByteConverter.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.http 2 | 3 | import okhttp3.ResponseBody 4 | import retrofit2.Converter 5 | import retrofit2.Retrofit 6 | import java.lang.reflect.Type 7 | 8 | class ByteConverter : Converter.Factory() { 9 | 10 | override fun responseBodyConverter( 11 | type: Type?, 12 | annotations: Array?, 13 | retrofit: Retrofit? 14 | ): Converter? { 15 | return Converter { value -> 16 | value.bytes() 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/http/EncodeConverter.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.http 2 | 3 | import io.legado.app.utils.UTF8BOMFighter 4 | import okhttp3.ResponseBody 5 | import io.legado.app.utils.EncodingDetect 6 | import retrofit2.Converter 7 | import retrofit2.Retrofit 8 | import java.lang.reflect.Type 9 | import java.nio.charset.Charset 10 | 11 | class EncodeConverter(private val encode: String? = null) : Converter.Factory() { 12 | 13 | override fun responseBodyConverter( 14 | type: Type?, 15 | annotations: Array?, 16 | retrofit: Retrofit? 17 | ): Converter? { 18 | return Converter { value -> 19 | val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes()) 20 | encode?.let { return@Converter String(responseBytes, Charset.forName(encode)) } 21 | 22 | var charsetName: String? = null 23 | val mediaType = value.contentType() 24 | //根据http头判断 25 | if (mediaType != null) { 26 | val charset = mediaType.charset() 27 | charsetName = charset?.displayName() 28 | } 29 | 30 | if (charsetName == null) { 31 | charsetName = EncodingDetect.getHtmlEncode(responseBytes) 32 | } 33 | 34 | String(responseBytes, Charset.forName(charsetName)) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/http/RequestMethod.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.http 2 | 3 | enum class RequestMethod { 4 | GET, POST 5 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/http/Res.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.http 2 | 3 | data class Res(val url: String, val body: String?) -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/http/StrResponse.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.http 2 | 3 | import okhttp3.* 4 | import okhttp3.Response.Builder 5 | 6 | /** 7 | * An HTTP response. 8 | */ 9 | @Suppress("unused", "MemberVisibilityCanBePrivate") 10 | class StrResponse { 11 | var raw: Response 12 | private set 13 | var body: String? = null 14 | private set 15 | var errorBody: ResponseBody? = null 16 | private set 17 | 18 | constructor(rawResponse: Response, body: String?) { 19 | this.raw = rawResponse 20 | this.body = body 21 | } 22 | 23 | constructor(url: String, body: String?) { 24 | raw = Builder() 25 | .code(200) 26 | .message("OK") 27 | .protocol(Protocol.HTTP_1_1) 28 | .request(Request.Builder().url(url).build()) 29 | .build() 30 | this.body = body 31 | } 32 | 33 | constructor(rawResponse: Response, errorBody: ResponseBody?) { 34 | this.raw = rawResponse 35 | this.errorBody = errorBody 36 | } 37 | 38 | fun raw() = raw 39 | 40 | fun url(): String { 41 | raw.networkResponse?.let { 42 | return it.request.url.toString() 43 | } 44 | return raw.request.url.toString() 45 | } 46 | 47 | val url: String get() = url() 48 | 49 | fun body() = body 50 | 51 | fun code(): Int { 52 | return raw.code 53 | } 54 | 55 | fun message(): String { 56 | return raw.message 57 | } 58 | 59 | fun headers(): Headers { 60 | return raw.headers 61 | } 62 | 63 | fun isSuccessful(): Boolean = raw.isSuccessful 64 | 65 | fun errorBody(): ResponseBody? { 66 | return errorBody 67 | } 68 | 69 | override fun toString(): String { 70 | return raw.toString() 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/help/http/api/CookieManager.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.help.http.api 2 | 3 | interface CookieManager { 4 | 5 | /** 6 | * 保存cookie 7 | */ 8 | fun setCookie(url: String, cookie: String?) 9 | 10 | /** 11 | * 替换cookie 12 | */ 13 | fun replaceCookie(url: String, cookie: String) 14 | 15 | /** 16 | * 获取cookie 17 | */ 18 | fun getCookie(url: String): String 19 | 20 | /** 21 | * 移除cookie 22 | */ 23 | fun removeCookie(url: String) 24 | 25 | fun cookieToMap(cookie: String): MutableMap 26 | 27 | fun mapToCookie(cookieMap: Map?): String? 28 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/lib/icu4j/CharsetRecognizer.java: -------------------------------------------------------------------------------- 1 | // © 2016 and later: Unicode, Inc. and others. 2 | // License & terms of use: http://www.unicode.org/copyright.html 3 | /** 4 | * ****************************************************************************** 5 | * Copyright (C) 2005-2012, International Business Machines Corporation and * 6 | * others. All Rights Reserved. * 7 | * ****************************************************************************** 8 | */ 9 | package io.legado.app.lib.icu4j; 10 | 11 | /** 12 | * Abstract class for recognizing a single charset. 13 | * Part of the implementation of ICU's CharsetDetector. 14 | *

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

20 | * Instances of CharsetDetector DO NOT have or maintain 21 | * state pertaining to a specific match or detect operation. 22 | * The WILL be shared by multiple instances of CharsetDetector. 23 | * They encapsulate const charset-specific information. 24 | */ 25 | abstract class CharsetRecognizer { 26 | /** 27 | * Get the IANA name of this charset. 28 | * 29 | * @return the charset name. 30 | */ 31 | abstract String getName(); 32 | 33 | /** 34 | * Get the ISO language code for this charset. 35 | * 36 | * @return the language code, or null if the language cannot be determined. 37 | */ 38 | public String getLanguage() { 39 | return null; 40 | } 41 | 42 | /** 43 | * Test the match of this charset with the input text data 44 | * which is obtained via the CharsetDetector object. 45 | * 46 | * @param det The CharsetDetector, which contains the input text 47 | * to be checked for being in this charset. 48 | * @return A CharsetMatch object containing details of match 49 | * with this charset, or null if there was no match. 50 | */ 51 | abstract CharsetMatch match(CharsetDetector det); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/Debug.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.model 2 | 3 | import io.legado.app.data.entities.Book 4 | import io.legado.app.data.entities.BookChapter 5 | import io.legado.app.model.webBook.WebBook 6 | import mu.KotlinLogging 7 | 8 | private val logger = KotlinLogging.logger {} 9 | 10 | object Debug : DebugLog{ 11 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/DebugLog.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.model 2 | 3 | import io.legado.app.data.entities.Book 4 | import io.legado.app.data.entities.BookChapter 5 | import mu.KotlinLogging 6 | import okhttp3.logging.HttpLoggingInterceptor 7 | 8 | private val logger = KotlinLogging.logger {} 9 | 10 | interface DebugLog: HttpLoggingInterceptor.Logger { 11 | fun log( 12 | sourceUrl: String? = "", 13 | msg: String? = "", 14 | isHtml: Boolean = false 15 | ) { 16 | logger.info("sourceUrl: {}, msg: {}", sourceUrl, msg) 17 | } 18 | 19 | override fun log(message: String) { 20 | logger.debug(message) 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/README.md: -------------------------------------------------------------------------------- 1 | # 放置一些模块类 2 | * analyzeRule 书源规则解析 3 | * localBook 本地书籍解析 4 | * rss 订阅规则解析 5 | * webBook 获取网络书籍 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/analyzeRule/AnalyzeByRegex.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.model.analyzeRule 2 | 3 | import java.util.* 4 | import java.util.regex.Pattern 5 | 6 | object AnalyzeByRegex { 7 | 8 | fun getElement(res: String, regs: Array, index: Int = 0): List? { 9 | var vIndex = index 10 | val resM = Pattern.compile(regs[vIndex]).matcher(res) 11 | if (!resM.find()) { 12 | return null 13 | } 14 | // 判断索引的规则是最后一个规则 15 | return if (vIndex + 1 == regs.size) { 16 | // 新建容器 17 | val info = arrayListOf() 18 | for (groupIndex in 0..resM.groupCount()) { 19 | info.add(resM.group(groupIndex)!!) 20 | } 21 | info 22 | } else { 23 | val result = StringBuilder() 24 | do { 25 | result.append(resM.group()) 26 | } while (resM.find()) 27 | getElement(result.toString(), regs, ++vIndex) 28 | } 29 | } 30 | 31 | fun getElements(res: String, regs: Array, index: Int = 0): List> { 32 | var vIndex = index 33 | val resM = Pattern.compile(regs[vIndex]).matcher(res) 34 | if (!resM.find()) { 35 | return arrayListOf() 36 | } 37 | // 判断索引的规则是最后一个规则 38 | if (vIndex + 1 == regs.size) { 39 | // 创建书息缓存数组 40 | val books = ArrayList>() 41 | // 提取列表 42 | do { 43 | // 新建容器 44 | val info = arrayListOf() 45 | for (groupIndex in 0..resM.groupCount()) { 46 | info.add(resM.group(groupIndex) ?: "") 47 | } 48 | books.add(info) 49 | } while (resM.find()) 50 | return books 51 | } else { 52 | val result = StringBuilder() 53 | do { 54 | result.append(resM.group()) 55 | } while (resM.find()) 56 | return getElements(result.toString(), regs, ++vIndex) 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/analyzeRule/RuleData.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.model.analyzeRule 2 | 3 | import io.legado.app.utils.GSON 4 | 5 | class RuleData : RuleDataInterface { 6 | 7 | override val variableMap by lazy { 8 | hashMapOf() 9 | } 10 | 11 | override fun putVariable(key: String, value: String?) { 12 | if (value == null) { 13 | variableMap.remove(key) 14 | } else { 15 | variableMap[key] = value 16 | } 17 | } 18 | 19 | fun getVariable(): String? { 20 | if (variableMap.isEmpty()) { 21 | return null 22 | } 23 | return GSON.toJson(variableMap) 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/analyzeRule/RuleDataInterface.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.model.analyzeRule 2 | 3 | interface RuleDataInterface { 4 | 5 | val variableMap: HashMap 6 | 7 | fun putVariable(key: String, value: String?) 8 | 9 | fun getVariable(key: String): String? { 10 | return variableMap[key] 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/model/rss/Rss.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.model.rss 2 | 3 | import io.legado.app.data.entities.RssArticle 4 | import io.legado.app.data.entities.RssSource 5 | import io.legado.app.help.coroutine.Coroutine 6 | import io.legado.app.model.DebugLog 7 | import io.legado.app.model.analyzeRule.AnalyzeRule 8 | import io.legado.app.model.analyzeRule.AnalyzeUrl 9 | import io.legado.app.model.analyzeRule.RuleData 10 | import io.legado.app.utils.NetworkUtils 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlin.coroutines.CoroutineContext 14 | 15 | object Rss { 16 | suspend fun getArticles( 17 | sortName: String, 18 | sortUrl: String, 19 | rssSource: RssSource, 20 | page: Int, 21 | debugLog: DebugLog? 22 | ): Pair, String?> { 23 | val ruleData = RuleData() 24 | val analyzeUrl = AnalyzeUrl( 25 | sortUrl, 26 | page = page, 27 | source = rssSource, 28 | ruleData = ruleData, 29 | headerMapF = rssSource.getHeaderMap() 30 | ) 31 | val body = analyzeUrl.getStrResponseAwait(debugLog = debugLog).body 32 | // debugLog?.log(rssSource.sourceUrl, "┌获取链接内容:${sortUrl}") 33 | // debugLog?.log(rssSource.sourceUrl, "└\n${body}") 34 | return RssParserByRule.parseXML(sortName, sortUrl, body, rssSource, ruleData, debugLog) 35 | } 36 | 37 | suspend fun getContent( 38 | rssArticle: RssArticle, 39 | ruleContent: String, 40 | rssSource: RssSource, 41 | debugLog: DebugLog? 42 | ): String { 43 | val analyzeUrl = AnalyzeUrl( 44 | rssArticle.link, 45 | baseUrl = rssArticle.origin, 46 | source = rssSource, 47 | ruleData = rssArticle, 48 | headerMapF = rssSource.getHeaderMap() 49 | ) 50 | val body = analyzeUrl.getStrResponseAwait(debugLog = debugLog).body 51 | // debugLog?.log(rssSource.sourceUrl, "┌获取链接内容:${rssArticle.link}") 52 | // debugLog?.log(rssSource.sourceUrl, "└\n${body}") 53 | val analyzeRule = AnalyzeRule(rssArticle, rssSource) 54 | analyzeRule.setContent(body) 55 | .setBaseUrl(NetworkUtils.getAbsoluteURL(rssArticle.origin, rssArticle.link)) 56 | return analyzeRule.getString(ruleContent) 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/AnkoHelps.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | inline fun attempt(f: () -> T): AttemptResult { 4 | var value: T? = null 5 | var error: Throwable? = null 6 | try { 7 | value = f() 8 | } catch(t: Throwable) { 9 | error = t 10 | } 11 | return AttemptResult(value, error) 12 | } 13 | 14 | data class AttemptResult @PublishedApi internal constructor(val value: T?, val error: Throwable?) { 15 | inline fun then(f: (T) -> R): AttemptResult { 16 | if (isError) { 17 | @Suppress("UNCHECKED_CAST") 18 | return this as AttemptResult 19 | } 20 | 21 | return attempt { f(value as T) } 22 | } 23 | 24 | inline val isError: Boolean 25 | get() = error != null 26 | 27 | inline val hasValue: Boolean 28 | get() = error == null 29 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/EncodingDetect.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | import io.legado.app.lib.icu4j.CharsetDetector 4 | import org.jsoup.Jsoup 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.nio.charset.StandardCharsets 8 | import java.util.* 9 | 10 | /** 11 | * 自动获取文件的编码 12 | * */ 13 | @Suppress("MemberVisibilityCanBePrivate", "unused") 14 | object EncodingDetect { 15 | 16 | fun getHtmlEncode(bytes: ByteArray): String? { 17 | try { 18 | val doc = Jsoup.parse(String(bytes, StandardCharsets.UTF_8)) 19 | val metaTags = doc.getElementsByTag("meta") 20 | var charsetStr: String 21 | for (metaTag in metaTags) { 22 | charsetStr = metaTag.attr("charset") 23 | if (!charsetStr.isEmpty()) { 24 | return charsetStr 25 | } 26 | val content = metaTag.attr("content") 27 | val httpEquiv = metaTag.attr("http-equiv") 28 | if (httpEquiv.lowercase(Locale.getDefault()) == "content-type") { 29 | charsetStr = if (content.lowercase(Locale.getDefault()).contains("charset")) { 30 | content.substring( 31 | content.lowercase(Locale.getDefault()) 32 | .indexOf("charset") + "charset=".length 33 | ) 34 | } else { 35 | content.substring(content.lowercase(Locale.getDefault()).indexOf(";") + 1) 36 | } 37 | if (!charsetStr.isEmpty()) { 38 | return charsetStr 39 | } 40 | } 41 | } 42 | } catch (ignored: Exception) { 43 | } 44 | return getEncode(bytes) 45 | } 46 | 47 | fun getEncode(bytes: ByteArray): String { 48 | val match = CharsetDetector().setText(bytes).detect() 49 | return match?.name ?: "UTF-8" 50 | } 51 | 52 | /** 53 | * 得到文件的编码 54 | */ 55 | fun getEncode(filePath: String): String { 56 | return getEncode(File(filePath)) 57 | } 58 | 59 | /** 60 | * 得到文件的编码 61 | */ 62 | fun getEncode(file: File): String { 63 | val tempByte = getFileBytes(file) 64 | return getEncode(tempByte) 65 | } 66 | 67 | private fun getFileBytes(file: File?): ByteArray { 68 | val byteArray = ByteArray(8000) 69 | try { 70 | FileInputStream(file).use { 71 | it.read(byteArray) 72 | } 73 | } catch (e: Exception) { 74 | System.err.println("Error: $e") 75 | } 76 | return byteArray 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/FileExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | import java.io.File 4 | 5 | fun File.getFile(vararg subDirFiles: String): File { 6 | val path = FileUtils.getPath(this, *subDirFiles) 7 | return File(path) 8 | } 9 | 10 | fun File.exists(vararg subDirFiles: String): Boolean { 11 | return getFile(*subDirFiles).exists() 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/HtmlFormatter.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | import io.legado.app.model.analyzeRule.AnalyzeUrl 4 | import java.net.URL 5 | import java.util.regex.Pattern 6 | 7 | object HtmlFormatter { 8 | private val wrapHtmlRegex = "]*>".toRegex() 9 | private val commentRegex = "".toRegex() //注释 10 | private val notImgHtmlRegex = "])[^<>]*>".toRegex() 11 | private val otherHtmlRegex = "])[^<>]*>".toRegex() 12 | private val formatImagePattern = Pattern.compile( 13 | "]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|]*data-[^=]*= *\"([^\"]*)\"[^>]*>|]*src *= *\"([^\"]*)\"[^>]*>", 14 | Pattern.CASE_INSENSITIVE 15 | ) 16 | 17 | fun format(html: String?, otherRegex: Regex = otherHtmlRegex): String { 18 | html ?: return "" 19 | return html.replace(wrapHtmlRegex, "\n") 20 | .replace(commentRegex, "") 21 | .replace(otherRegex, "") 22 | .replace("\\s*\\n+\\s*".toRegex(), "\n  ") 23 | .replace("^[\\n\\s]+".toRegex(), "  ") 24 | .replace("[\\n\\s]+$".toRegex(), "") 25 | } 26 | 27 | fun formatKeepImg(html: String?, redirectUrl: URL? = null): String { 28 | html ?: return "" 29 | val keepImgHtml = format(html, notImgHtmlRegex) 30 | 31 | //正则的“|”处于顶端而不处于()中时,具有类似||的熔断效果,故以此机制简化原来的代码 32 | val matcher = formatImagePattern.matcher(keepImgHtml) 33 | var appendPos = 0 34 | val sb = StringBuffer() 35 | while (matcher.find()) { 36 | var param = "" 37 | sb.append( 38 | keepImgHtml.substring(appendPos, matcher.start()), "" 50 | ) 51 | appendPos = matcher.end() 52 | } 53 | if (appendPos < keepImgHtml.length) sb.append( 54 | keepImgHtml.substring( 55 | appendPos, 56 | keepImgHtml.length 57 | ) 58 | ) 59 | return sb.toString() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/JsonExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | import com.jayway.jsonpath.* 4 | 5 | val jsonPath: ParseContext by lazy { 6 | JsonPath.using( 7 | Configuration.builder() 8 | .options(Option.SUPPRESS_EXCEPTIONS) 9 | .build() 10 | ) 11 | } 12 | 13 | fun ReadContext.readString(path: String): String? = this.read(path, String::class.java) 14 | 15 | fun ReadContext.readBool(path: String): Boolean? = this.read(path, Boolean::class.java) 16 | 17 | fun ReadContext.readInt(path: String): Int? = this.read(path, Int::class.java) 18 | 19 | fun ReadContext.readLong(path: String): Long? = this.read(path, Long::class.java) 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/JsoupExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | import org.jsoup.internal.StringUtil 4 | import org.jsoup.nodes.CDataNode 5 | import org.jsoup.nodes.Element 6 | import org.jsoup.nodes.Node 7 | import org.jsoup.nodes.TextNode 8 | import org.jsoup.select.NodeTraversor 9 | import org.jsoup.select.NodeVisitor 10 | 11 | 12 | fun Element.textArray(): Array { 13 | val sb = StringUtil.borrowBuilder() 14 | NodeTraversor.traverse(object : NodeVisitor { 15 | override fun head(node: Node, depth: Int) { 16 | if (node is TextNode) { 17 | appendNormalisedText(sb, node) 18 | } else if (node is Element) { 19 | if (sb.isNotEmpty() && 20 | (node.isBlock || node.tag().name == "br") && 21 | !lastCharIsWhitespace(sb) 22 | ) sb.append("\n") 23 | } 24 | } 25 | 26 | override fun tail(node: Node, depth: Int) { 27 | if (node is Element) { 28 | if (node.isBlock && node.nextSibling() is TextNode 29 | && !lastCharIsWhitespace(sb) 30 | ) { 31 | sb.append("\n") 32 | } 33 | } 34 | } 35 | }, this) 36 | val text = StringUtil.releaseBuilder(sb).trim { it <= ' ' } 37 | return text.splitNotBlank("\n") 38 | } 39 | 40 | private fun appendNormalisedText(sb: StringBuilder, textNode: TextNode) { 41 | val text = textNode.wholeText 42 | if (preserveWhitespace(textNode.parentNode()) || textNode is CDataNode) 43 | sb.append(text) 44 | else StringUtil.appendNormalisedWhitespace(sb, text, lastCharIsWhitespace(sb)) 45 | } 46 | 47 | private fun preserveWhitespace(node: Node?): Boolean { 48 | if (node is Element) { 49 | var el = node as Element? 50 | var i = 0 51 | do { 52 | if (el!!.tag().preserveWhitespace()) return true 53 | el = el.parent() 54 | i++ 55 | } while (i < 6 && el != null) 56 | } 57 | return false 58 | } 59 | 60 | private fun lastCharIsWhitespace(sb: java.lang.StringBuilder): Boolean { 61 | return sb.isNotEmpty() && sb[sb.length - 1] == ' ' 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/LogUtils.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package io.legado.app.utils 4 | 5 | fun Throwable.printOnDebug() { 6 | printStackTrace() 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/MD5Utils.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | import java.security.MessageDigest 4 | import java.security.NoSuchAlgorithmException 5 | 6 | /** 7 | * 将字符串转化为MD5 8 | */ 9 | 10 | object MD5Utils { 11 | 12 | fun md5Encode(str: String?): String { 13 | if (str == null) return "" 14 | var reStr = "" 15 | try { 16 | val md5:MessageDigest = MessageDigest.getInstance("MD5") 17 | val bytes:ByteArray = md5.digest(str.toByteArray()) 18 | val stringBuffer:StringBuilder = StringBuilder() 19 | for (b in bytes) { 20 | val bt:Int = b.toInt() and 0xff 21 | if (bt < 16) { 22 | stringBuffer.append(0) 23 | } 24 | stringBuffer.append(Integer.toHexString(bt)) 25 | } 26 | reStr = stringBuffer.toString() 27 | } catch (e: NoSuchAlgorithmException) { 28 | e.printStackTrace() 29 | } 30 | 31 | return reStr 32 | } 33 | 34 | fun md5Encode16(str: String): String { 35 | var reStr = md5Encode(str) 36 | reStr = reStr.substring(8, 24) 37 | return reStr 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/TextUtils.java: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils; 2 | 3 | import java.util.Iterator; 4 | 5 | public class TextUtils { 6 | 7 | public static boolean isEmpty(CharSequence str) { 8 | return str == null || str.length() == 0; 9 | } 10 | 11 | 12 | /** 13 | * Returns a string containing the tokens joined by delimiters. 14 | * 15 | * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string 16 | * "null" will be used as the delimiter. 17 | * @param tokens an array objects to be joined. Strings will be formed from the objects by 18 | * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If 19 | * tokens is an empty array, an empty string will be returned. 20 | */ 21 | public static String join(CharSequence delimiter, Object[] tokens) { 22 | final int length = tokens.length; 23 | if (length == 0) { 24 | return ""; 25 | } 26 | final StringBuilder sb = new StringBuilder(); 27 | sb.append(tokens[0]); 28 | for (int i = 1; i < length; i++) { 29 | sb.append(delimiter); 30 | sb.append(tokens[i]); 31 | } 32 | return sb.toString(); 33 | } 34 | 35 | 36 | /** 37 | * Returns a string containing the tokens joined by delimiters. 38 | * 39 | * @param delimiter a CharSequence that will be inserted between the tokens. If null, the string 40 | * "null" will be used as the delimiter. 41 | * @param tokens an array objects to be joined. Strings will be formed from the objects by 42 | * calling object.toString(). If tokens is null, a NullPointerException will be thrown. If 43 | * tokens is empty, an empty string will be returned. 44 | */ 45 | public static String join(CharSequence delimiter, Iterable tokens) { 46 | final Iterator it = tokens.iterator(); 47 | if (!it.hasNext()) { 48 | return ""; 49 | } 50 | final StringBuilder sb = new StringBuilder(); 51 | sb.append(it.next()); 52 | while (it.hasNext()) { 53 | sb.append(delimiter); 54 | sb.append(it.next()); 55 | } 56 | return sb.toString(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/ThrowableExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | val Throwable.msg: String 4 | get() { 5 | val stackTrace = stackTraceToString() 6 | val lMsg = this.localizedMessage ?: "noErrorMsg" 7 | return when { 8 | stackTrace.isNotEmpty() -> stackTrace 9 | else -> lMsg 10 | } 11 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/UTF8BOMFighter.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | object UTF8BOMFighter { 4 | private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) 5 | 6 | fun removeUTF8BOM(xmlText: String): String { 7 | val bytes = xmlText.toByteArray() 8 | val containsBOM = (bytes.size > 3 9 | && bytes[0] == UTF8_BOM_BYTES[0] 10 | && bytes[1] == UTF8_BOM_BYTES[1] 11 | && bytes[2] == UTF8_BOM_BYTES[2]) 12 | if (containsBOM) { 13 | return String(bytes, 3, bytes.size - 3) 14 | } 15 | return xmlText 16 | } 17 | 18 | fun removeUTF8BOM(bytes: ByteArray): ByteArray { 19 | val containsBOM = (bytes.size > 3 20 | && bytes[0] == UTF8_BOM_BYTES[0] 21 | && bytes[1] == UTF8_BOM_BYTES[1] 22 | && bytes[2] == UTF8_BOM_BYTES[2]) 23 | if (containsBOM) { 24 | val copy = ByteArray(bytes.size - 3) 25 | System.arraycopy(bytes, 3, copy, 0, bytes.size - 3) 26 | return copy 27 | } 28 | return bytes 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/io/legado/app/utils/Utf8BomUtils.kt: -------------------------------------------------------------------------------- 1 | package io.legado.app.utils 2 | 3 | @Suppress("unused") 4 | object Utf8BomUtils { 5 | private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) 6 | 7 | fun removeUTF8BOM(xmlText: String): String { 8 | val bytes = xmlText.toByteArray() 9 | val containsBOM = (bytes.size > 3 10 | && bytes[0] == UTF8_BOM_BYTES[0] 11 | && bytes[1] == UTF8_BOM_BYTES[1] 12 | && bytes[2] == UTF8_BOM_BYTES[2]) 13 | if (containsBOM) { 14 | return String(bytes, 3, bytes.size - 3) 15 | } 16 | return xmlText 17 | } 18 | 19 | fun removeUTF8BOM(bytes: ByteArray): ByteArray { 20 | val containsBOM = (bytes.size > 3 21 | && bytes[0] == UTF8_BOM_BYTES[0] 22 | && bytes[1] == UTF8_BOM_BYTES[1] 23 | && bytes[2] == UTF8_BOM_BYTES[2]) 24 | if (containsBOM) { 25 | val copy = ByteArray(bytes.size - 3) 26 | System.arraycopy(bytes, 3, copy, 0, bytes.size - 3) 27 | return copy 28 | } 29 | return bytes 30 | } 31 | 32 | fun hasBom(bytes: ByteArray): Boolean { 33 | return (bytes.size > 3 34 | && bytes[0] == UTF8_BOM_BYTES[0] 35 | && bytes[1] == UTF8_BOM_BYTES[1] 36 | && bytes[2] == UTF8_BOM_BYTES[2]) 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/Constants.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib; 2 | 3 | 4 | public interface Constants { 5 | 6 | String CHARACTER_ENCODING = "UTF-8"; 7 | String DOCTYPE_XHTML = ""; 8 | String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml"; 9 | String EPUB_GENERATOR_NAME = "Ag2S EpubLib"; 10 | String EPUB_DUOKAN_NAME = "DK-SONGTI"; 11 | char FRAGMENT_SEPARATOR_CHAR = '#'; 12 | String DEFAULT_TOC_ID = "toc"; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/browsersupport/NavigationEventListener.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.browsersupport; 2 | 3 | /** 4 | * Implemented by classes that want to be notified if the user moves to 5 | * another location in the book. 6 | * 7 | * @author paul 8 | * 9 | */ 10 | public interface NavigationEventListener { 11 | 12 | /** 13 | * Called whenever the user navigates to another position in the book. 14 | * 15 | * @param navigationEvent f 16 | */ 17 | void navigationPerformed(NavigationEvent navigationEvent); 18 | } -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/browsersupport/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides classes that help make an epub reader application. 3 | * 4 | * These classes have no dependencies on graphic toolkits, they're purely 5 | * to help with the browsing/navigation logic. 6 | */ 7 | package me.ag2s.epublib.browsersupport; 8 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/Author.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | 4 | 5 | import me.ag2s.epublib.util.StringUtil; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Represents one of the authors of the book 11 | * 12 | * @author paul 13 | */ 14 | public class Author implements Serializable { 15 | 16 | private static final long serialVersionUID = 6663408501416574200L; 17 | 18 | private String firstname; 19 | private String lastname; 20 | private Relator relator = Relator.AUTHOR; 21 | 22 | public Author(String singleName) { 23 | this("", singleName); 24 | } 25 | 26 | public Author(String firstname, String lastname) { 27 | this.firstname = firstname; 28 | this.lastname = lastname; 29 | } 30 | 31 | public String getFirstname() { 32 | return firstname; 33 | } 34 | 35 | public void setFirstname(String firstname) { 36 | this.firstname = firstname; 37 | } 38 | 39 | public String getLastname() { 40 | return lastname; 41 | } 42 | 43 | public void setLastname(String lastname) { 44 | this.lastname = lastname; 45 | } 46 | 47 | 48 | @Override 49 | @SuppressWarnings("NullableProblems") 50 | public String toString() { 51 | return this.lastname + ", " + this.firstname; 52 | } 53 | 54 | public int hashCode() { 55 | return StringUtil.hashCode(firstname, lastname); 56 | } 57 | 58 | public boolean equals(Object authorObject) { 59 | if (!(authorObject instanceof Author)) { 60 | return false; 61 | } 62 | Author other = (Author) authorObject; 63 | return StringUtil.equals(firstname, other.firstname) 64 | && StringUtil.equals(lastname, other.lastname); 65 | } 66 | 67 | /** 68 | * 设置贡献者的角色 69 | * 70 | * @param code 角色编号 71 | */ 72 | 73 | public void setRole(String code) { 74 | Relator result = Relator.byCode(code); 75 | if (result == null) { 76 | result = Relator.AUTHOR; 77 | } 78 | this.relator = result; 79 | } 80 | 81 | public Relator getRelator() { 82 | return relator; 83 | } 84 | 85 | 86 | public void setRelator(Relator relator) { 87 | this.relator = relator; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/EpubResourceProvider.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.zip.ZipEntry; 6 | import java.util.zip.ZipFile; 7 | 8 | /** 9 | * @author jake 10 | */ 11 | public class EpubResourceProvider implements LazyResourceProvider { 12 | 13 | private final String epubFilename; 14 | 15 | /** 16 | * @param epubFilename the file name for the epub we're created from. 17 | */ 18 | public EpubResourceProvider(String epubFilename) { 19 | this.epubFilename = epubFilename; 20 | } 21 | 22 | @Override 23 | public InputStream getResourceStream(String href) throws IOException { 24 | ZipFile zipFile = new ZipFile(epubFilename); 25 | ZipEntry zipEntry = zipFile.getEntry(href); 26 | if (zipEntry == null) { 27 | zipFile.close(); 28 | throw new IllegalStateException( 29 | "Cannot find entry " + href + " in epub file " + epubFilename); 30 | } 31 | return new ResourceInputStream(zipFile.getInputStream(zipEntry), zipFile); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/FileResourceProvider.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | /** 9 | * 用于创建epub,添加大文件(如大量图片)时容易OOM,使用LazyResource,避免OOM. 10 | * 11 | */ 12 | 13 | public class FileResourceProvider implements LazyResourceProvider { 14 | //需要导入资源的父目录 15 | String dir; 16 | 17 | /** 18 | * 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。 19 | * @param parentDir 文件的目录 20 | */ 21 | public FileResourceProvider(String parentDir) { 22 | this.dir = parentDir; 23 | } 24 | 25 | /** 26 | * 创建一个文件夹里面文件夹的LazyResourceProvider,用于LazyResource。 27 | * @param parentFile 文件夹 28 | */ 29 | @SuppressWarnings("unused") 30 | public FileResourceProvider(File parentFile) { 31 | this.dir = parentFile.getPath(); 32 | } 33 | 34 | /** 35 | * 根据子文件名href,再父目录下读取文件获取FileInputStream 36 | * @param href 子文件名href 37 | * @return 对应href的FileInputStream 38 | * @throws IOException 抛出IOException 39 | */ 40 | @Override 41 | public InputStream getResourceStream(String href) throws IOException { 42 | return new FileInputStream(new File(dir, href)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/LazyResourceProvider.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | 6 | /** 7 | * @author jake 8 | */ 9 | public interface LazyResourceProvider { 10 | 11 | InputStream getResourceStream(String href) throws IOException; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/ManifestItemProperties.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | @SuppressWarnings("unused") 3 | public enum ManifestItemProperties implements ManifestProperties { 4 | COVER_IMAGE("cover-image"), 5 | MATHML("mathml"), 6 | NAV("nav"), 7 | REMOTE_RESOURCES("remote-resources"), 8 | SCRIPTED("scripted"), 9 | SVG("svg"), 10 | SWITCH("switch"); 11 | 12 | private final String name; 13 | 14 | ManifestItemProperties(String name) { 15 | this.name = name; 16 | } 17 | 18 | public String getName() { 19 | return name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/ManifestItemRefProperties.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | @SuppressWarnings("unused") 3 | public enum ManifestItemRefProperties implements ManifestProperties { 4 | PAGE_SPREAD_LEFT("page-spread-left"), 5 | PAGE_SPREAD_RIGHT("page-spread-right"); 6 | 7 | private final String name; 8 | 9 | ManifestItemRefProperties(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/ManifestProperties.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | public interface ManifestProperties { 4 | 5 | String getName(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/MediaType.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.Serializable; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | 7 | /** 8 | * MediaType is used to tell the type of content a resource is. 9 | * 10 | * Examples of mediatypes are image/gif, text/css and application/xhtml+xml 11 | * 12 | * All allowed mediaTypes are maintained bye the MediaTypeService. 13 | * 14 | * @see MediaTypes 15 | * 16 | * @author paul 17 | */ 18 | public class MediaType implements Serializable { 19 | 20 | private static final long serialVersionUID = -7256091153727506788L; 21 | private final String name; 22 | private final String defaultExtension; 23 | private final Collection extensions; 24 | 25 | public MediaType(String name, String defaultExtension) { 26 | this(name, defaultExtension, new String[]{defaultExtension}); 27 | } 28 | 29 | public MediaType(String name, String defaultExtension, 30 | String[] extensions) { 31 | this(name, defaultExtension, Arrays.asList(extensions)); 32 | } 33 | 34 | public int hashCode() { 35 | if (name == null) { 36 | return 0; 37 | } 38 | return name.hashCode(); 39 | } 40 | 41 | public MediaType(String name, String defaultExtension, 42 | Collection mextensions) { 43 | super(); 44 | this.name = name; 45 | this.defaultExtension = defaultExtension; 46 | this.extensions = mextensions; 47 | } 48 | 49 | public String getName() { 50 | return name; 51 | } 52 | 53 | 54 | public String getDefaultExtension() { 55 | return defaultExtension; 56 | } 57 | 58 | 59 | public Collection getExtensions() { 60 | return extensions; 61 | } 62 | 63 | public boolean equals(Object otherMediaType) { 64 | if (!(otherMediaType instanceof MediaType)) { 65 | return false; 66 | } 67 | return name.equals(((MediaType) otherMediaType).getName()); 68 | } 69 | @SuppressWarnings("NullableProblems") 70 | public String toString() { 71 | return name; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/ResourceInputStream.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.FilterInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.zip.ZipFile; 7 | 8 | /** 9 | * A wrapper class for closing a ZipFile object when the InputStream derived 10 | * from it is closed. 11 | * 12 | * @author ttopalov 13 | */ 14 | public class ResourceInputStream extends FilterInputStream { 15 | 16 | private final ZipFile zipFile; 17 | 18 | /** 19 | * Constructor. 20 | * 21 | * @param in 22 | * The InputStream object. 23 | * @param zipFile 24 | * The ZipFile object. 25 | */ 26 | public ResourceInputStream(InputStream in, ZipFile zipFile) { 27 | super(in); 28 | this.zipFile = zipFile; 29 | } 30 | 31 | @Override 32 | public void close() throws IOException { 33 | super.close(); 34 | zipFile.close(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/ResourceReference.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | public class ResourceReference implements Serializable { 6 | 7 | private static final long serialVersionUID = 2596967243557743048L; 8 | 9 | protected Resource resource; 10 | 11 | public ResourceReference(Resource resource) { 12 | this.resource = resource; 13 | } 14 | 15 | 16 | public Resource getResource() { 17 | return resource; 18 | } 19 | 20 | /** 21 | * Besides setting the resource it also sets the fragmentId to null. 22 | * 23 | * @param resource resource 24 | */ 25 | public void setResource(Resource resource) { 26 | this.resource = resource; 27 | } 28 | 29 | 30 | /** 31 | * The id of the reference referred to. 32 | * 33 | * null of the reference is null or has a null id itself. 34 | * 35 | * @return The id of the reference referred to. 36 | */ 37 | public String getResourceId() { 38 | if (resource != null) { 39 | return resource.getId(); 40 | } 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/SpineReference.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | 6 | /** 7 | * A Section of a book. 8 | * Represents both an item in the package document and a item in the index. 9 | * 10 | * @author paul 11 | */ 12 | public class SpineReference extends ResourceReference implements Serializable { 13 | 14 | private static final long serialVersionUID = -7921609197351510248L; 15 | private boolean linear;//default = true; 16 | 17 | public SpineReference(Resource resource) { 18 | this(resource, true); 19 | } 20 | 21 | 22 | public SpineReference(Resource resource, boolean linear) { 23 | super(resource); 24 | this.linear = linear; 25 | } 26 | 27 | /** 28 | * Linear denotes whether the section is Primary or Auxiliary. 29 | * Usually the cover page has linear set to false and all the other sections 30 | * have it set to true. 31 | *

32 | * It's an optional property that readers may also ignore. 33 | * 34 | *

primary or auxiliary is useful for Reading Systems which 35 | * opt to present auxiliary content differently than primary content. 36 | * For example, a Reading System might opt to render auxiliary content in 37 | * a popup window apart from the main window which presents the primary 38 | * content. (For an example of the types of content that may be considered 39 | * auxiliary, refer to the example below and the subsequent discussion.)
40 | * 41 | * @return whether the section is Primary or Auxiliary. 42 | * @see OPF Spine specification 43 | */ 44 | public boolean isLinear() { 45 | return linear; 46 | } 47 | 48 | public void setLinear(boolean linear) { 49 | this.linear = linear; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/domain/TOCReference.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.domain; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | 8 | /** 9 | * An item in the Table of Contents. 10 | * 11 | * @see TableOfContents 12 | * 13 | * @author paul 14 | */ 15 | public class TOCReference extends TitledResourceReference 16 | implements Serializable { 17 | 18 | private static final long serialVersionUID = 5787958246077042456L; 19 | private List children; 20 | private static final Comparator COMPARATOR_BY_TITLE_IGNORE_CASE = (tocReference1, tocReference2) -> String.CASE_INSENSITIVE_ORDER.compare(tocReference1.getTitle(), tocReference2.getTitle()); 21 | @Deprecated 22 | public TOCReference() { 23 | this(null, null, null); 24 | } 25 | 26 | public TOCReference(String name, Resource resource) { 27 | this(name, resource, null); 28 | } 29 | 30 | public TOCReference(String name, Resource resource, String fragmentId) { 31 | this(name, resource, fragmentId, new ArrayList<>()); 32 | } 33 | 34 | public TOCReference(String title, Resource resource, String fragmentId, 35 | List children) { 36 | super(resource, title, fragmentId); 37 | this.children = children; 38 | } 39 | @SuppressWarnings("unused") 40 | public static Comparator getComparatorByTitleIgnoreCase() { 41 | return COMPARATOR_BY_TITLE_IGNORE_CASE; 42 | } 43 | 44 | public List getChildren() { 45 | return children; 46 | } 47 | 48 | public TOCReference addChildSection(TOCReference childSection) { 49 | this.children.add(childSection); 50 | return childSection; 51 | } 52 | 53 | public void setChildren(List children) { 54 | this.children = children; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/epub/BookProcessor.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.epub; 2 | 3 | import me.ag2s.epublib.domain.EpubBook; 4 | 5 | /** 6 | * Post-processes a book. 7 | * 8 | * Can be used to clean up a book after reading or before writing. 9 | * 10 | * @author paul 11 | */ 12 | public interface BookProcessor { 13 | 14 | /** 15 | * A BookProcessor that returns the input book unchanged. 16 | */ 17 | BookProcessor IDENTITY_BOOKPROCESSOR = book -> book; 18 | 19 | EpubBook processBook(EpubBook book); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/epub/BookProcessorPipeline.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.epub; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | 7 | import me.ag2s.epublib.domain.EpubBook; 8 | 9 | /** 10 | * A book processor that combines several other bookprocessors 11 | *

12 | * Fixes coverpage/coverimage. 13 | * Cleans up the XHTML. 14 | * 15 | * @author paul.siegmann 16 | */ 17 | @SuppressWarnings("unused declaration") 18 | public class BookProcessorPipeline implements BookProcessor { 19 | 20 | private static final String TAG= BookProcessorPipeline.class.getName(); 21 | private List bookProcessors; 22 | 23 | public BookProcessorPipeline() { 24 | this(null); 25 | } 26 | 27 | public BookProcessorPipeline(List bookProcessingPipeline) { 28 | this.bookProcessors = bookProcessingPipeline; 29 | } 30 | 31 | @Override 32 | public EpubBook processBook(EpubBook book) { 33 | if (bookProcessors == null) { 34 | return book; 35 | } 36 | for (BookProcessor bookProcessor : bookProcessors) { 37 | try { 38 | book = bookProcessor.processBook(book); 39 | } catch (Exception e) { 40 | // Log.e(TAG, e.getMessage(), e); 41 | e.printStackTrace(); 42 | } 43 | } 44 | return book; 45 | } 46 | 47 | public void addBookProcessor(BookProcessor bookProcessor) { 48 | if (this.bookProcessors == null) { 49 | bookProcessors = new ArrayList<>(); 50 | } 51 | this.bookProcessors.add(bookProcessor); 52 | } 53 | 54 | public void addBookProcessors(Collection bookProcessors) { 55 | if (this.bookProcessors == null) { 56 | this.bookProcessors = new ArrayList<>(); 57 | } 58 | this.bookProcessors.addAll(bookProcessors); 59 | } 60 | 61 | 62 | public List getBookProcessors() { 63 | return bookProcessors; 64 | } 65 | 66 | 67 | public void setBookProcessingPipeline( 68 | List bookProcessingPipeline) { 69 | this.bookProcessors = bookProcessingPipeline; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/epub/HtmlProcessor.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.epub; 2 | 3 | import me.ag2s.epublib.domain.Resource; 4 | import java.io.OutputStream; 5 | @SuppressWarnings("unused") 6 | public interface HtmlProcessor { 7 | 8 | void processHtmlResource(Resource resource, OutputStream out); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/util/CollectionUtil.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.util; 2 | 3 | import java.util.Collection; 4 | import java.util.Enumeration; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | 8 | public class CollectionUtil { 9 | 10 | /** 11 | * Wraps an Enumeration around an Iterator 12 | * @author paul.siegmann 13 | * 14 | * @param 15 | */ 16 | private static class IteratorEnumerationAdapter implements Enumeration { 17 | 18 | private final Iterator iterator; 19 | 20 | public IteratorEnumerationAdapter(Iterator iter) { 21 | this.iterator = iter; 22 | } 23 | 24 | @Override 25 | public boolean hasMoreElements() { 26 | return iterator.hasNext(); 27 | } 28 | 29 | @Override 30 | public T nextElement() { 31 | return iterator.next(); 32 | } 33 | } 34 | 35 | /** 36 | * Creates an Enumeration out of the given Iterator. 37 | * @param g 38 | * @param it g 39 | * @return an Enumeration created out of the given Iterator. 40 | */ 41 | @SuppressWarnings("unused") 42 | public static Enumeration createEnumerationFromIterator( 43 | Iterator it) { 44 | return new IteratorEnumerationAdapter<>(it); 45 | } 46 | 47 | 48 | /** 49 | * Returns the first element of the list, null if the list is null or empty. 50 | * 51 | * @param f 52 | * @param list f 53 | * @return the first element of the list, null if the list is null or empty. 54 | */ 55 | public static T first(List list) { 56 | if (list == null || list.isEmpty()) { 57 | return null; 58 | } 59 | return list.get(0); 60 | } 61 | 62 | /** 63 | * Whether the given collection is null or has no elements. 64 | * 65 | * @param collection g 66 | * @return Whether the given collection is null or has no elements. 67 | */ 68 | public static boolean isEmpty(Collection collection) { 69 | return collection == null || collection.isEmpty(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/util/NoCloseOutputStream.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.util; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | /** 7 | * OutputStream with the close() disabled. 8 | * We write multiple documents to a ZipOutputStream. 9 | * Some of the formatters call a close() after writing their data. 10 | * We don't want them to do that, so we wrap regular OutputStreams in this NoCloseOutputStream. 11 | * 12 | * @author paul 13 | */ 14 | @SuppressWarnings("unused") 15 | public class NoCloseOutputStream extends OutputStream { 16 | 17 | private final OutputStream outputStream; 18 | 19 | public NoCloseOutputStream(OutputStream outputStream) { 20 | this.outputStream = outputStream; 21 | } 22 | 23 | @Override 24 | public void write(int b) throws IOException { 25 | outputStream.write(b); 26 | } 27 | 28 | /** 29 | * A close() that does not call it's parent's close() 30 | */ 31 | public void close() { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/util/NoCloseWriter.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.epublib.util; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | 6 | /** 7 | * Writer with the close() disabled. 8 | * We write multiple documents to a ZipOutputStream. 9 | * Some of the formatters call a close() after writing their data. 10 | * We don't want them to do that, so we wrap regular Writers in this NoCloseWriter. 11 | * 12 | * @author paul 13 | */ 14 | @SuppressWarnings("unused") 15 | public class NoCloseWriter extends Writer { 16 | 17 | private final Writer writer; 18 | 19 | public NoCloseWriter(Writer writer) { 20 | this.writer = writer; 21 | } 22 | 23 | @Override 24 | public void close() { 25 | } 26 | 27 | @Override 28 | public void flush() throws IOException { 29 | writer.flush(); 30 | } 31 | 32 | @Override 33 | public void write(char[] cbuf, int off, int len) throws IOException { 34 | writer.write(cbuf, off, len); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package me.ag2s.epublib.util.commons.io; 19 | 20 | import java.io.IOException; 21 | import java.util.Objects; 22 | import java.util.function.Consumer; 23 | 24 | /** 25 | * Like {@link Consumer} but throws {@link IOException}. 26 | * 27 | * @param the type of the input to the operations. 28 | * @since 2.7 29 | */ 30 | @FunctionalInterface 31 | public interface IOConsumer { 32 | 33 | /** 34 | * Performs this operation on the given argument. 35 | * 36 | * @param t the input argument 37 | * @throws IOException if an I/O error occurs. 38 | */ 39 | void accept(T t) throws IOException; 40 | 41 | /** 42 | * Returns a composed {@code IoConsumer} that performs, in sequence, this operation followed by the {@code after} 43 | * operation. If performing either operation throws an exception, it is relayed to the caller of the composed 44 | * operation. If performing this operation throws an exception, the {@code after} operation will not be performed. 45 | * 46 | * @param after the operation to perform after this operation 47 | * @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after} 48 | * operation 49 | * @throws NullPointerException if {@code after} is null 50 | */ 51 | @SuppressWarnings("unused") 52 | default IOConsumer andThen(final IOConsumer after) { 53 | Objects.requireNonNull(after); 54 | return (final T t) -> { 55 | accept(t); 56 | after.accept(t); 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/umdlib/domain/UmdBook.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.umdlib.domain; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | import me.ag2s.umdlib.tool.WrapOutputStream; 7 | 8 | public class UmdBook { 9 | 10 | public int getNum() { 11 | return num; 12 | } 13 | 14 | public void setNum(int num) { 15 | this.num = num; 16 | } 17 | 18 | private int num; 19 | 20 | 21 | /** Header Part of UMD book */ 22 | private UmdHeader header = new UmdHeader(); 23 | /** 24 | * Detail chapters Part of UMD book 25 | * (include Titles & Contents of each chapter) 26 | */ 27 | private UmdChapters chapters = new UmdChapters(); 28 | 29 | /** Cover Part of UMD book (for example, and JPEG file) */ 30 | private UmdCover cover = new UmdCover(); 31 | 32 | /** End Part of UMD book */ 33 | private UmdEnd end = new UmdEnd(); 34 | 35 | /** 36 | * Build the UMD file. 37 | * @param os 38 | * @throws IOException 39 | */ 40 | public void buildUmd(OutputStream os) throws IOException { 41 | WrapOutputStream wos = new WrapOutputStream(os); 42 | 43 | header.buildHeader(wos); 44 | chapters.buildChapters(wos); 45 | cover.buildCover(wos); 46 | end.buildEnd(wos); 47 | } 48 | 49 | public UmdHeader getHeader() { 50 | return header; 51 | } 52 | 53 | public void setHeader(UmdHeader header) { 54 | this.header = header; 55 | } 56 | 57 | public UmdChapters getChapters() { 58 | return chapters; 59 | } 60 | 61 | public void setChapters(UmdChapters chapters) { 62 | this.chapters = chapters; 63 | } 64 | 65 | public UmdCover getCover() { 66 | return cover; 67 | } 68 | 69 | public void setCover(UmdCover cover) { 70 | this.cover = cover; 71 | } 72 | 73 | public UmdEnd getEnd() { 74 | return end; 75 | } 76 | 77 | public void setEnd(UmdEnd end) { 78 | this.end = end; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/umdlib/domain/UmdEnd.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.umdlib.domain; 2 | 3 | import java.io.IOException; 4 | 5 | import me.ag2s.umdlib.tool.WrapOutputStream; 6 | 7 | /** 8 | * End part of UMD book, nothing to be special 9 | * 10 | * @author Ray Liang (liangguanhui@qq.com) 11 | * 2009-12-20 12 | */ 13 | public class UmdEnd { 14 | 15 | public void buildEnd(WrapOutputStream wos) throws IOException { 16 | wos.writeBytes('#', 0x0C, 0, 0x01, 0x09); 17 | wos.writeInt(wos.getWritten() + 4); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/ag2s/umdlib/tool/WrapOutputStream.java: -------------------------------------------------------------------------------- 1 | package me.ag2s.umdlib.tool; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | 6 | public class WrapOutputStream extends OutputStream { 7 | 8 | private OutputStream os; 9 | private int written; 10 | 11 | public WrapOutputStream(OutputStream os) { 12 | this.os = os; 13 | } 14 | 15 | private void incCount(int value) { 16 | int temp = written + value; 17 | if (temp < 0) { 18 | temp = Integer.MAX_VALUE; 19 | } 20 | written = temp; 21 | } 22 | 23 | // it is different from the writeInt of DataOutputStream 24 | public void writeInt(int v) throws IOException { 25 | os.write((v >>> 0) & 0xFF); 26 | os.write((v >>> 8) & 0xFF); 27 | os.write((v >>> 16) & 0xFF); 28 | os.write((v >>> 24) & 0xFF); 29 | incCount(4); 30 | } 31 | 32 | public void writeByte(byte b) throws IOException { 33 | write(b); 34 | } 35 | 36 | public void writeByte(int n) throws IOException { 37 | write(n); 38 | } 39 | 40 | public void writeBytes(byte ... bytes) throws IOException { 41 | write(bytes); 42 | } 43 | 44 | public void writeBytes(int ... vals) throws IOException { 45 | for (int v : vals) { 46 | write(v); 47 | } 48 | } 49 | 50 | public void write(byte[] b, int off, int len) throws IOException { 51 | os.write(b, off, len); 52 | incCount(len); 53 | } 54 | 55 | public void write(byte[] b) throws IOException { 56 | os.write(b); 57 | incCount(b.length); 58 | } 59 | 60 | public void write(int b) throws IOException { 61 | os.write(b); 62 | incCount(1); 63 | } 64 | 65 | ///////////////////////////////////////////////// 66 | 67 | public void close() throws IOException { 68 | os.close(); 69 | } 70 | 71 | public void flush() throws IOException { 72 | os.flush(); 73 | } 74 | 75 | public boolean equals(Object obj) { 76 | return os.equals(obj); 77 | } 78 | 79 | public int hashCode() { 80 | return os.hashCode(); 81 | } 82 | 83 | public String toString() { 84 | return os.toString(); 85 | } 86 | 87 | public int getWritten() { 88 | return written; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/kxml2/wap/Wbxml.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | * sell copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | * IN THE SOFTWARE. */ 20 | 21 | package org.kxml2.wap; 22 | 23 | 24 | /** contains the WBXML constants */ 25 | 26 | 27 | public interface Wbxml { 28 | 29 | static public final int SWITCH_PAGE = 0; 30 | static public final int END = 1; 31 | static public final int ENTITY = 2; 32 | static public final int STR_I = 3; 33 | static public final int LITERAL = 4; 34 | static public final int EXT_I_0 = 0x40; 35 | static public final int EXT_I_1 = 0x41; 36 | static public final int EXT_I_2 = 0x42; 37 | static public final int PI = 0x43; 38 | static public final int LITERAL_C = 0x44; 39 | static public final int EXT_T_0 = 0x80; 40 | static public final int EXT_T_1 = 0x81; 41 | static public final int EXT_T_2 = 0x82; 42 | static public final int STR_T = 0x83; 43 | static public final int LITERAL_A = 0x084; 44 | static public final int EXT_0 = 0x0c0; 45 | static public final int EXT_1 = 0x0c1; 46 | static public final int EXT_2 = 0x0c2; 47 | static public final int OPAQUE = 0x0c3; 48 | static public final int LITERAL_AC = 0x0c4; 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/org.xmlpull.v1.XmlPullParserFactory: -------------------------------------------------------------------------------- 1 | org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer 2 | -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: prod 4 | 5 | reader: 6 | app: 7 | storagePath: 'storage' 8 | showUI: false 9 | debug: false 10 | packaged: false 11 | secure: false 12 | inviteCode: "" 13 | secureKey: "" 14 | proxy: false 15 | proxyType: "HTTP" 16 | proxyHost: "" 17 | proxyPort: "" 18 | proxyUsername: "" 19 | proxyPassword: "" 20 | cacheChapterContent: true 21 | userLimit: 50 22 | userBookLimit: 200 23 | debugLog: false 24 | autoClearInactiveUser: 0 25 | 26 | server: 27 | port: 8080 28 | webUrl: http://localhost:${reader.server.port} 29 | 30 | logging: 31 | path: "./logs" -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | reader: 2 | app: 3 | storagePath: storage 4 | showUI: false 5 | debug: false 6 | packaged: false 7 | secure: false 8 | inviteCode: "" 9 | secureKey: "" 10 | proxy: false 11 | proxyType: "HTTP" 12 | proxyHost: "" 13 | proxyPort: "" 14 | proxyUsername: "" 15 | proxyPassword: "" 16 | cacheChapterContent: true 17 | userLimit: 50 18 | userBookLimit: 200 19 | debugLog: false 20 | autoClearInactiveUser: 0 21 | 22 | server: 23 | port: 8080 24 | webUrl: http://localhost:${reader.server.port} 25 | 26 | logging: 27 | path: "./logs" -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ██████  ███████  █████  ██████  ███████ ██████  2 | ██   ██ ██      ██   ██ ██   ██ ██      ██   ██  3 | ██████  █████  ███████ ██  ██ █████  ██████   4 | ██   ██ ██     ██   ██ ██  ██ ██     ██   ██  5 | ██  ██ ███████ ██  ██ ██████  ███████ ██  ██  6 |                                               7 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-arch-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 35 | 36 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-base-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | ]]> 38 | 39 | 40 | 45 | ]]> 46 | 47 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-bdo-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 28 | 29 | 30 | 34 | 35 | 36 | ]]> 37 | 38 | 39 | 45 | ]]> 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkpres-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 29 | 30 | 31 | ]]> 32 | 33 | 34 | 38 | ]]> 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-blkstruct-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 31 | 32 | 33 | ]]> 34 | 35 | 36 | 40 | ]]> 41 | 42 | 43 | 46 | 47 | 48 | ]]> 49 | 50 | 51 | 55 | ]]> 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-charent-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 23 | 24 | 27 | %xhtml-lat1; 28 | 29 | 32 | %xhtml-symbol; 33 | 34 | 37 | %xhtml-special; 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-edit-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | ]]> 36 | 37 | 38 | 44 | ]]> 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | ]]> 56 | 57 | 58 | 64 | ]]> 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-hypertext-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | ]]> 38 | 39 | 40 | 52 | ]]> 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-image-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 24 | 25 | 30 | 31 | 32 | 34 | 35 | 36 | ]]> 37 | 38 | 39 | 49 | ]]> 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstruct-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | ]]> 36 | 37 | 38 | 42 | ]]> 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | ]]> 54 | 55 | 56 | 60 | ]]> 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-inlstyle-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 24 | 25 | 28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-link-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | ]]> 44 | 45 | 46 | 57 | ]]> 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-meta-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | ]]> 34 | 35 | 36 | 45 | ]]> 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-object-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | ]]> 40 | 41 | 42 | 58 | ]]> 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-param-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | ]]> 35 | 36 | 37 | 46 | ]]> 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-pres-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 23 | 24 | 25 | 29 | %xhtml-inlpres.mod;]]> 30 | 31 | 32 | 36 | %xhtml-blkpres.mod;]]> 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-script-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | ]]> 35 | 36 | 37 | 47 | ]]> 48 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | ]]> 59 | 60 | 61 | 65 | ]]> 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-ssismap-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 23 | 24 | 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-style-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | ]]> 34 | 35 | 36 | 46 | ]]> 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/resources/dtd/www.w3.org/TR/xhtml-modularization/DTD/xhtml-text-1.mod: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 23 | 24 | 25 | 29 | %xhtml-inlstruct.mod;]]> 30 | 31 | 32 | 36 | %xhtml-inlphras.mod;]]> 37 | 38 | 39 | 43 | %xhtml-blkstruct.mod;]]> 44 | 45 | 46 | 50 | %xhtml-blkphras.mod;]]> 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/resources/epub/chapter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chapter 6 | 7 | 8 | 9 | 10 |

{title}

11 | {content} 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/epub/cover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cover 6 | 15 | 16 | 17 |
18 |

{name}

19 |
{author} / 著
20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/epub/intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intro 6 | 7 | 8 | 9 | 10 |

内容简介

{intro} 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/epub/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/epub/logo.png -------------------------------------------------------------------------------- /src/main/resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/icons/128x128.png -------------------------------------------------------------------------------- /src/main/resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/icons/16x16.png -------------------------------------------------------------------------------- /src/main/resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/icons/24x24.png -------------------------------------------------------------------------------- /src/main/resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/icons/32x32.png -------------------------------------------------------------------------------- /src/main/resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/icons/48x48.png -------------------------------------------------------------------------------- /src/main/resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/icons/64x64.png -------------------------------------------------------------------------------- /src/main/resources/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/src/main/resources/images/loading.gif -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | ${logPath}/reader-%d{yyyy-MM-dd}.%i.log 16 | 17 | 100MB 18 | 19 | 20 | 21 | [%thread] %d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/test/java/com/htmake/reader/ReaderApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.htmake.reader; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ReaderApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /vetur.config.js: -------------------------------------------------------------------------------- 1 | // vetur.config.js 2 | /** @type {import('vls').VeturConfig} */ 3 | module.exports = { 4 | // **optional** default: `{}` 5 | // override vscode settings 6 | // Notice: It only affects the settings used by Vetur. 7 | settings: { 8 | "vetur.useWorkspaceDependencies": true, 9 | "vetur.experimental.templateInterpolationService": true 10 | }, 11 | // **optional** default: `[{ root: './' }]` 12 | projects: [ 13 | './web' 14 | ] 15 | } -------------------------------------------------------------------------------- /web/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.5% 2 | last 2 chrome version 3 | last 2 firefox version 4 | last 2 edge version 5 | last 2 opera version 6 | last 22 ios versions 7 | last 65 android versions 8 | last 4 ie versions -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | globals: { 8 | workbox: "writable" 9 | }, 10 | rules: { 11 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 12 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 13 | }, 14 | parserOptions: { 15 | parser: "babel-eslint" 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | #Carlo build output 24 | /dist_carlo 25 | /.profile 26 | dist*.zip 27 | 28 | yarn.lock 29 | pnpm-lock.yaml -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # 「阅读3.0」 web 端(可设置IP) 2 | 3 | 本程序为「阅读3.0」的配套 web 端,需要保证手机和电脑在同一局域网内,然后手机端打开 web 服务。 4 | 5 | 在线地址 http://alanskycn.gitee.io/vip/reader/ 6 | 7 | ## 具体实现 8 | 9 | 使用 Vue2 开发 10 | 11 | ## 功能特性 12 | 13 | - 本地存储阅读记录与设置 14 | - 阅读主题切换 15 | - 夜间模式 16 | - 字号调节 17 | - 字体调节 18 | - 阅读宽度调节 19 | 20 | ## 使用方法 21 | 22 | ```shell 23 | yarn install 24 | #安装项目 25 | yarn serve 26 | #开发模式 27 | yarn build 28 | #打包 29 | yarn lint 30 | #格式化代码 31 | ``` 32 | - ~~点击`Star`自动编译,可在Actions查看~~ 33 | - ~~编译失败,可先点击`Unstar`,再点击`Star`重新开始~~ 34 | -------------------------------------------------------------------------------- /web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@vue/app", 5 | { 6 | polyfills: ["es.promise", "es.symbol"] 7 | } 8 | ] 9 | ], 10 | plugins: [ 11 | [ 12 | "component", 13 | { 14 | libraryName: "element-ui", 15 | styleLibraryName: "theme-chalk" 16 | } 17 | ] 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ] 5 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reader", 3 | "version": "2.5.4", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "sync": "yarn build && rm -rf ../src/main/resources/web && mv dist ../src/main/resources/web" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "codejar": "^3.5.0", 14 | "core-js": "^3.3.2", 15 | "element-ui": "^2.15.9", 16 | "localforage": "^1.10.0", 17 | "prismjs": "^1.25.0", 18 | "register-service-worker": "^1.7.1", 19 | "sortablejs": "^1.15.0", 20 | "stylus": "^0.54.7", 21 | "stylus-loader": "^3.0.2", 22 | "vue": "^2.6.10", 23 | "vue-lazyload": "^1.3.3", 24 | "vue-router": "^3.1.3", 25 | "vuex": "^3.1.1" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^4.0.0", 29 | "@vue/cli-plugin-eslint": "^4.0.0", 30 | "@vue/cli-plugin-pwa": "^4.0.0", 31 | "@vue/cli-plugin-router": "^4.0.0", 32 | "@vue/cli-service": "^4.0.0", 33 | "@vue/eslint-config-prettier": "^5.0.0", 34 | "babel-eslint": "^10.0.3", 35 | "babel-plugin-component": "^1.1.1", 36 | "eslint": "^5.16.0", 37 | "eslint-plugin-prettier": "^3.1.1", 38 | "eslint-plugin-vue": "^5.0.0", 39 | "prettier": "^1.18.2", 40 | "vue-cli-plugin-element": "^1.0.1", 41 | "vue-template-compiler": "^2.6.10" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /web/public/bg/午后沙滩.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/午后沙滩.jpg -------------------------------------------------------------------------------- /web/public/bg/宁静夜色.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/宁静夜色.jpg -------------------------------------------------------------------------------- /web/public/bg/山水墨影.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/山水墨影.jpg -------------------------------------------------------------------------------- /web/public/bg/山水画.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/山水画.jpg -------------------------------------------------------------------------------- /web/public/bg/护眼漫绿.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/护眼漫绿.jpg -------------------------------------------------------------------------------- /web/public/bg/新羊皮纸.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/新羊皮纸.jpg -------------------------------------------------------------------------------- /web/public/bg/明媚倾城.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/明媚倾城.jpg -------------------------------------------------------------------------------- /web/public/bg/深宫魅影.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/深宫魅影.jpg -------------------------------------------------------------------------------- /web/public/bg/清新时光.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/清新时光.jpg -------------------------------------------------------------------------------- /web/public/bg/羊皮纸1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/羊皮纸1.jpg -------------------------------------------------------------------------------- /web/public/bg/羊皮纸2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/羊皮纸2.jpg -------------------------------------------------------------------------------- /web/public/bg/羊皮纸3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/羊皮纸3.jpg -------------------------------------------------------------------------------- /web/public/bg/羊皮纸4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/羊皮纸4.jpg -------------------------------------------------------------------------------- /web/public/bg/边彩画布.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/bg/边彩画布.jpg -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/public/img/icons/android-chrome-maskable-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/android-chrome-maskable-192x192.png -------------------------------------------------------------------------------- /web/public/img/icons/android-chrome-maskable-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/android-chrome-maskable-512x512.png -------------------------------------------------------------------------------- /web/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /web/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /web/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /web/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /web/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /web/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /web/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /web/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /web/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /web/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /web/src/assets/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | @font-face { 3 | font-family: "iconfont"; 4 | src: url("./iconfont.woff") format("woff"); 5 | } 6 | 7 | @font-face { 8 | font-family: "reader-iconfont"; /* Project id 2841133 */ 9 | src: url('./reader-iconfont.woff2?t=1657702223137') format('woff2'), 10 | url('./reader-iconfont.woff?t=1657702223137') format('woff'), 11 | url('./reader-iconfont.ttf?t=1657702223137') format('truetype'); 12 | } 13 | 14 | .reader-iconfont { 15 | font-family: "reader-iconfont" !important; 16 | font-size: 16px; 17 | font-style: normal; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | .reader-icon-shezhi:before { 23 | content: "\e654"; 24 | } 25 | 26 | .reader-icon-volume-off:before { 27 | content: "\e631"; 28 | } 29 | 30 | .reader-icon-volume:before { 31 | content: "\e632"; 32 | } 33 | 34 | .reader-icon-15s:before { 35 | content: "\e61f"; 36 | } 37 | 38 | .reader-icon-jian15s:before { 39 | content: "\e620"; 40 | } 41 | 42 | .reader-icon-player-pause:before { 43 | content: "\ea2b"; 44 | } 45 | 46 | .reader-icon-player-forward-step:before { 47 | content: "\ea2c"; 48 | } 49 | 50 | .reader-icon-player-backward-step:before { 51 | content: "\ea2d"; 52 | } 53 | 54 | .reader-icon-player-play:before { 55 | content: "\ea2e"; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /web/src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /web/src/assets/fonts/reader-iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/fonts/reader-iconfont.ttf -------------------------------------------------------------------------------- /web/src/assets/fonts/reader-iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/fonts/reader-iconfont.woff -------------------------------------------------------------------------------- /web/src/assets/fonts/reader-iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/fonts/reader-iconfont.woff2 -------------------------------------------------------------------------------- /web/src/assets/imgs/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/github.png -------------------------------------------------------------------------------- /web/src/assets/imgs/github2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/github2.png -------------------------------------------------------------------------------- /web/src/assets/imgs/mpcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/mpcode.jpg -------------------------------------------------------------------------------- /web/src/assets/imgs/noCover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/noCover.jpeg -------------------------------------------------------------------------------- /web/src/assets/imgs/noImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/noImage.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/body_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/body_0.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/body_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/body_1.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/body_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/body_2.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/body_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/body_3.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/body_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/body_5.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/body_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/body_6.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/content_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/content_0.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/content_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/content_1.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/content_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/content_2.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/content_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/content_3.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/content_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/content_5.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/content_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/content_6.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/popup_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/popup_0.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/popup_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/popup_1.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/popup_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/popup_2.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/popup_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/popup_3.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/popup_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/popup_5.png -------------------------------------------------------------------------------- /web/src/assets/imgs/themes/popup_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/imgs/themes/popup_6.png -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hectorqin/reader/774113b4507def3e967c817c9d9d1eee67a8fdab/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/src/components/MPCode.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | 51 | -------------------------------------------------------------------------------- /web/src/plugins/animate.js: -------------------------------------------------------------------------------- 1 | if (!window.requestAnimationFrame) { 2 | window.requestAnimationFrame = function(callback) { 3 | return setTimeout(callback, 1000 / 60); 4 | }; 5 | } 6 | 7 | // 动画执行函数 8 | function Animate(options) { 9 | var start = Date.now(); 10 | 11 | window.requestAnimationFrame(function _animate() { 12 | // timeFraction 从 0 逐渐增加到 1 13 | var timeFraction = (Date.now() - start) / options.duration; 14 | if (timeFraction > 1) timeFraction = 1; 15 | 16 | var progress = options.timing(timeFraction); // 动画当前进度 17 | options.draw(progress); // 绘制动画 18 | 19 | if (timeFraction < 1) { 20 | window.requestAnimationFrame(_animate); 21 | } else { 22 | options.onEnd && options.onEnd(); 23 | } 24 | }); 25 | } 26 | 27 | // 时序函数 28 | Animate.Timings = { 29 | // 线性函数 30 | linear: function(timeFraction) { 31 | return timeFraction; 32 | }, 33 | // 圆弧函数 34 | circle: function(timeFraction) { 35 | return 1 - Math.sin(Math.acos(timeFraction)); 36 | }, 37 | // 圆弧函数(与上一个相同) 38 | circle2: function(timeFraction) { 39 | return 1 - (1 - timeFraction ** 2) ** 0.5; 40 | }, 41 | // 反-弹跳函数 42 | bounce: function(timeFraction) { 43 | for (var a = 0, b = 1; (a += b), (b /= 2); ) { 44 | if (timeFraction >= (7 - 4 * a) / 11) { 45 | return ( 46 | -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) 47 | ); 48 | } 49 | } 50 | }, 51 | 52 | // 幂函数,x 为指数 53 | power: function(x, timeFraction) { 54 | return Math.pow(timeFraction, x); 55 | }, 56 | // 反弹函数,x 为弹性系数 57 | back: function(x, timeFraction) { 58 | return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x); 59 | }, 60 | // 伸缩函数,x 为初始范围 61 | elastic: function(x, timeFraction) { 62 | return ( 63 | Math.pow(2, 10 * (timeFraction - 1)) * 64 | Math.cos(((20 * Math.PI * x) / 3) * timeFraction) 65 | ); 66 | } 67 | }; 68 | 69 | // 工具函数 70 | Animate.Utils = { 71 | // 接受时序函数,返回时序函数的反函数 72 | makeEaseOut: function(timing) { 73 | return function easeOut(timeFraction) { 74 | return 1 - timing(1 - timeFraction); 75 | }; 76 | }, 77 | // 接受时序函数,返回时序函数的 easeInOut 变体 78 | makeEaseInOut: function(timing) { 79 | return function easeInOut(timeFraction) { 80 | if (timeFraction < 0.5) return timing(2 * timeFraction) / 2; 81 | else return 1 - timing(2 * (1 - timeFraction)) / 2; 82 | }; 83 | } 84 | }; 85 | 86 | export default Animate; 87 | -------------------------------------------------------------------------------- /web/src/plugins/cache.js: -------------------------------------------------------------------------------- 1 | export const setCache = (key, value) => { 2 | value = typeof value === "string" ? value : JSON.stringify(value); 3 | window.localStorage && window.localStorage.setItem(key, value); 4 | }; 5 | 6 | export const getCache = (key, defaultVal = null) => { 7 | let val = defaultVal; 8 | try { 9 | val = window.localStorage && window.localStorage.getItem(key); 10 | if (val === null) { 11 | return defaultVal; 12 | } 13 | if (val) { 14 | const parseVal = JSON.parse(val); 15 | if (parseVal !== null) { 16 | return parseVal; 17 | } 18 | } 19 | return val; 20 | } catch (error) { 21 | return val; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /web/src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { 3 | Button, 4 | Divider, 5 | MessageBox, 6 | Message, 7 | Breadcrumb, 8 | BreadcrumbItem, 9 | Table, 10 | TableColumn, 11 | Popover, 12 | Loading, 13 | Input, 14 | Select, 15 | Option, 16 | Tag, 17 | Collapse, 18 | CollapseItem, 19 | Dialog, 20 | Checkbox, 21 | CheckboxGroup, 22 | ColorPicker, 23 | Slider, 24 | Form, 25 | FormItem, 26 | Switch, 27 | Link, 28 | RadioGroup, 29 | RadioButton, 30 | Pagination, 31 | InputNumber, 32 | Image, 33 | Badge, 34 | Tabs, 35 | TabPane, 36 | Dropdown, 37 | DropdownItem, 38 | DropdownMenu 39 | } from "element-ui"; 40 | 41 | Vue.use(Button); 42 | Vue.use(Divider); 43 | Vue.use(Breadcrumb); 44 | Vue.use(BreadcrumbItem); 45 | Vue.use(Table); 46 | Vue.use(TableColumn); 47 | Vue.use(Popover); 48 | Vue.use(Input); 49 | Vue.use(Select); 50 | Vue.use(Option); 51 | Vue.use(Tag); 52 | Vue.use(Loading.directive); 53 | Vue.use(Collapse); 54 | Vue.use(CollapseItem); 55 | Vue.use(Dialog); 56 | Vue.use(Checkbox); 57 | Vue.use(CheckboxGroup); 58 | Vue.use(ColorPicker); 59 | Vue.use(Slider); 60 | Vue.use(Form); 61 | Vue.use(FormItem); 62 | Vue.use(Switch); 63 | Vue.use(Link); 64 | Vue.use(RadioGroup); 65 | Vue.use(RadioButton); 66 | Vue.use(Pagination); 67 | Vue.use(InputNumber); 68 | Vue.use(Image); 69 | Vue.use(Badge); 70 | Vue.use(Tabs); 71 | Vue.use(TabPane); 72 | Vue.use(Dropdown); 73 | Vue.use(DropdownItem); 74 | Vue.use(DropdownMenu); 75 | 76 | Vue.prototype.$msgbox = MessageBox; 77 | Vue.prototype.$message = Object.assign({}, Message, { 78 | info(message, duration) { 79 | const options = typeof message === "string" ? { message } : message; 80 | options.duration = duration || 1000; 81 | Message.info(options); 82 | }, 83 | error(message, duration) { 84 | const options = typeof message === "string" ? { message } : message; 85 | options.duration = duration || 2000; 86 | Message.error(options); 87 | }, 88 | success(message, duration) { 89 | const options = typeof message === "string" ? { message } : message; 90 | options.duration = duration || 1000; 91 | Message.success(options); 92 | } 93 | }); 94 | Vue.prototype.$ELEMENT = { 95 | zIndex: 2100 96 | }; 97 | Vue.prototype.$alert = MessageBox.alert; 98 | Vue.prototype.$confirm = MessageBox.confirm; 99 | Vue.prototype.$prompt = MessageBox.prompt; 100 | Vue.prototype.$loading = Loading.service; 101 | -------------------------------------------------------------------------------- /web/src/plugins/eventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | export default new Vue(); 3 | -------------------------------------------------------------------------------- /web/src/plugins/safe-json-stringify.js: -------------------------------------------------------------------------------- 1 | var hasProp = Object.prototype.hasOwnProperty; 2 | 3 | function throwsMessage(err) { 4 | return "[Throws: " + (err ? err.message : "?") + "]"; 5 | } 6 | 7 | function safeGetValueFromPropertyOnObject(obj, property) { 8 | if (hasProp.call(obj, property)) { 9 | try { 10 | return obj[property]; 11 | } catch (err) { 12 | return throwsMessage(err); 13 | } 14 | } 15 | 16 | return obj[property]; 17 | } 18 | 19 | function ensureProperties(obj) { 20 | var seen = []; // store references to objects we have seen before 21 | 22 | function visit(obj) { 23 | if (obj === null || typeof obj !== "object") { 24 | return obj; 25 | } 26 | 27 | if (seen.indexOf(obj) !== -1) { 28 | return "[Circular]"; 29 | } 30 | seen.push(obj); 31 | 32 | if (typeof obj.toJSON === "function") { 33 | try { 34 | var fResult = visit(obj.toJSON()); 35 | seen.pop(); 36 | return fResult; 37 | } catch (err) { 38 | return throwsMessage(err); 39 | } 40 | } 41 | 42 | if (Array.isArray(obj)) { 43 | var aResult = obj.map(visit); 44 | seen.pop(); 45 | return aResult; 46 | } 47 | 48 | var result = Object.keys(obj).reduce(function(result, prop) { 49 | // prevent faulty defined getter properties 50 | result[prop] = visit(safeGetValueFromPropertyOnObject(obj, prop)); 51 | return result; 52 | }, {}); 53 | seen.pop(); 54 | return result; 55 | } 56 | 57 | return visit(obj); 58 | } 59 | 60 | export const jsonEncode = function(data, replacer, space) { 61 | return JSON.stringify(ensureProperties(data), replacer, space); 62 | }; 63 | -------------------------------------------------------------------------------- /web/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from "register-service-worker"; 4 | 5 | export function registerServiceWorker() { 6 | try { 7 | if ( 8 | process.env.NODE_ENV === "production" && 9 | !window.getQueryString("nopwa") 10 | ) { 11 | register(`${process.env.BASE_URL}service-worker.js`, { 12 | ready() { 13 | // console.log( 14 | // "App is being served from cache by a service worker.\n" + 15 | // "For more details, visit https://goo.gl/AFskqB" 16 | // ); 17 | window.serviceWorkerReady = true; 18 | }, 19 | registered(registration) { 20 | // console.log("Service worker has been registered."); 21 | if (window.localStorage) { 22 | const currentVersion = window.localStorage.getItem( 23 | "READER_APP_BUILD_VERSION" 24 | ); 25 | const newVersion = process.env.VUE_APP_BUILD_VERSION; 26 | if (currentVersion !== newVersion) { 27 | registration.active.postMessage({ type: "SKIP_WAITING" }); 28 | window.localStorage.setItem( 29 | "READER_APP_BUILD_VERSION", 30 | newVersion 31 | ); 32 | } 33 | } 34 | } 35 | // cached() { 36 | // console.log("Content has been cached for offline use."); 37 | // }, 38 | // updatefound() { 39 | // console.log("New content is downloading."); 40 | // }, 41 | // updated() { 42 | // console.log("New content is available; please refresh."); 43 | // }, 44 | // offline() { 45 | // console.log( 46 | // "No internet connection found. App is running in offline mode." 47 | // ); 48 | // }, 49 | // error(error) { 50 | // console.error("Error during service worker registration:", error); 51 | // } 52 | }); 53 | } 54 | } catch (error) { 55 | // 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | 4 | Vue.use(VueRouter); 5 | 6 | const originalPush = VueRouter.prototype.push; 7 | 8 | VueRouter.prototype.push = function push(location) { 9 | return originalPush.call(this, location).catch(err => err); 10 | }; 11 | 12 | const routes = [ 13 | { 14 | path: "/", 15 | name: "index", 16 | component: () => 17 | import(/* webpackChunkName: "index" */ "../views/Index.vue") 18 | }, 19 | { 20 | path: "/reader", 21 | name: "Reader", 22 | component: () => 23 | import(/* webpackChunkName: "reader" */ "../views/Reader.vue") 24 | } 25 | ]; 26 | 27 | const router = new VueRouter({ 28 | // mode: "history", 29 | base: process.env.BASE_URL, 30 | routes 31 | }); 32 | 33 | export default router; 34 | --------------------------------------------------------------------------------