فهرست منبع

Merge remote-tracking branch 'upstream/master'

Seidko 4 سال پیش
والد
کامیت
d916d06b7f
43فایلهای تغییر یافته به همراه273 افزوده شده و 1198 حذف شده
  1. BIN
      app/cronetlib/cronet_api.jar
  2. BIN
      app/cronetlib/cronet_impl_common_java.jar
  3. 1 1
      app/src/main/assets/cronet.json
  4. 1 1
      app/src/main/assets/epub/main.css
  5. 5 0
      app/src/main/assets/updateLog.md
  6. 0 0
      app/src/main/assets/web/bookSource/css/app.1bd41080.css
  7. 0 0
      app/src/main/assets/web/bookSource/css/app.37e72839.css
  8. BIN
      app/src/main/assets/web/bookSource/favicon.ico
  9. BIN
      app/src/main/assets/web/bookSource/fonts/Cascadia-Code-Regular-2.97c09831.ttf
  10. 0 150
      app/src/main/assets/web/bookSource/index.css
  11. 1 439
      app/src/main/assets/web/bookSource/index.html
  12. 0 516
      app/src/main/assets/web/bookSource/index.js
  13. 0 0
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js
  14. 0 0
      app/src/main/assets/web/bookSource/js/app.2c3e42a8.js.map
  15. 0 0
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js
  16. 0 0
      app/src/main/assets/web/bookSource/js/app.836bd8bb.js.map
  17. 0 0
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js
  18. 0 0
      app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js.map
  19. 1 1
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  20. 14 4
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  21. 19 8
      app/src/main/java/io/legado/app/help/BookHelp.kt
  22. 3 1
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  23. 5 3
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  24. 48 0
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  25. 9 9
      app/src/main/java/io/legado/app/model/localBook/EpubFile.kt
  26. 11 8
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  27. 0 3
      app/src/main/java/io/legado/app/model/localBook/TextFile.kt
  28. 0 2
      app/src/main/java/io/legado/app/model/localBook/UmdFile.kt
  29. 8 2
      app/src/main/java/io/legado/app/ui/book/local/rule/TxtTocRuleActivity.kt
  30. 3 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  31. 3 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  32. 36 1
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  33. 2 2
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  34. 1 1
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  35. 47 4
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt
  36. 2 2
      app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt
  37. 8 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  38. 12 10
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
  39. 7 1
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  40. 0 18
      app/src/main/java/io/legado/app/utils/BitmapUtils.kt
  41. 1 1
      app/src/main/java/io/legado/app/utils/HtmlFormatter.kt
  42. 24 3
      app/src/main/java/io/legado/app/web/HttpServer.kt
  43. 1 1
      gradle.properties

BIN
app/cronetlib/cronet_api.jar


BIN
app/cronetlib/cronet_impl_common_java.jar


+ 1 - 1
app/src/main/assets/cronet.json

@@ -1 +1 @@
-{"arm64-v8a":"974957b4deb6281424de9efa33efe143","armeabi-v7a":"277d780f141a99df77fb78c79edec05f","x86_64":"12bd6ec5c9d90307498b6b44dd2e9cd1","x86":"e9d116221dfd9de432a3e11a239b5bd7","version":"105.0.5195.68"}
+{"arm64-v8a":"11ded68691faa98fb1ab552210c8d4a8","armeabi-v7a":"4a986b523de502fc85ba6ea51f6f7184","x86_64":"80a642112fe079f3d689a6420a27e926","x86":"e72bf103350222d212bcb0b641733d43","version":"105.0.5195.77"}

+ 1 - 1
app/src/main/assets/epub/main.css

@@ -203,7 +203,7 @@ h2.head {
     text-align: left;
     text-align: left;
     font-weight: bold;
     font-weight: bold;
     font-size: 1.1em;
     font-size: 1.1em;
-    margin: -3em 2em 2em 0;
+    margin: 1em 2em 2em 0;
     color: #3f83e8;
     color: #3f83e8;
     line-height: 140%;
     line-height: 140%;
 }
 }

+ 5 - 0
app/src/main/assets/updateLog.md

@@ -11,6 +11,11 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
 
+**2022/09/03**
+
+* 更新cronet: 105.0.5195.77
+* 更新web端书源编辑 by jgckM
+
 **2022/08/31**
 **2022/08/31**
 
 
 * 更新cronet: 105.0.5195.68
 * 更新cronet: 105.0.5195.68

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/css/app.1bd41080.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/css/app.37e72839.css


BIN
app/src/main/assets/web/bookSource/favicon.ico


BIN
app/src/main/assets/web/bookSource/fonts/Cascadia-Code-Regular-2.97c09831.ttf


+ 0 - 150
app/src/main/assets/web/bookSource/index.css

@@ -1,150 +0,0 @@
-body {
-  margin: 0;
-}
-.editor {
-  display: flex;
-  align-items: stretch;
-}
-.setbox,
-.menu,
-.outbox {
-  flex: 1;
-  display: flex;
-  flex-flow: column;
-  max-height: 100vh;
-  overflow-y: auto;
-}
-.menu {
-  justify-content: center;
-  max-width: 90px;
-  margin: 0 5px;
-}
-.menu .button {
-  width: 90px;
-  height: 30px;
-  min-height: 30px;
-  margin: 5px 0px;
-  cursor: pointer;
-}
-@keyframes stroker {
-  0% {
-    stroke-dashoffset: 0;
-  }
-  100% {
-    stroke-dashoffset: -240;
-  }
-}
-.button rect {
-  width: 100%;
-  height: 100%;
-  fill: transparent;
-  stroke: #666;
-  stroke-width: 2px;
-}
-.button rect.busy {
-  stroke: #fd1850;
-  stroke-dasharray: 30 90;
-  animation: stroker 1s linear infinite;
-}
-.button text {
-  text-anchor: middle;
-  dominant-baseline: middle;
-}
-.setbox {
-  min-width: 40em;
-}
-.rules {
-  overflow: auto;
-}
-.tabbox {
-  flex: 1;
-  display: flex;
-  flex-flow: column;
-}
-.rules > * {
-  display: flex;
-  margin: 2px 0;
-}
-.rules textarea {
-  flex: 1;
-  margin-left: 5px;
-}
-.rules > *,
-.rules > * > div,
-.rules textarea {
-  min-height: 1em;
-}
-textarea {
-  word-break: break-all;
-}
-.tabtitle {
-  display: flex;
-  z-index: 1;
-  justify-content: flex-end;
-}
-.tabtitle > div {
-  cursor: pointer;
-  padding: 1px 10px 0 10px;
-  border-bottom: 3px solid transparent;
-  font-weight: bold;
-}
-.tabtitle > .this {
-  color: #4f9da6;
-  border-bottom-color: #4ebbe4;
-}
-.tabbody {
-  flex: 1;
-  display: flex;
-  margin-top: -1px;
-  border: 1px solid #a9a9a9;
-  height: 0;
-}
-.tabbody > * {
-  flex: 1;
-  flex-flow: column;
-  display: none;
-}
-.tabbody > .this {
-  display: flex;
-}
-.tabbody > * > .titlebar {
-  display: flex;
-}
-.tabbody > * > .titlebar > * {
-  flex: 1;
-  margin: 1px 1px 1px 1px;
-}
-.tabbody > * > .context {
-  flex: 1;
-  flex-flow: column;
-  border: 0;
-  padding: 5px;
-  overflow-y: auto;
-}
-.tabbody > * > .inputbox {
-  border: 0;
-  border-bottom: #a9a9a9 solid 1px;
-  height: 15px;
-  text-align: center;
-}
-.link > * {
-  display: flex;
-  margin: 5px;
-  border-bottom: 1px solid;
-  text-decoration: none;
-}
-#RuleList > label > * {
-  background: #eee;
-  padding-left: 3px;
-  margin: 2px 0;
-  cursor: pointer;
-}
-#RuleList input[type="radio"] {
-  display: none;
-}
-#RuleList input[type="radio"]:checked + * {
-  background: #15cda8;
-}
-.isError {
-  color: #ff0000;
-}

+ 1 - 439
app/src/main/assets/web/bookSource/index.html

@@ -1,439 +1 @@
-<!DOCTYPE html>
-<html>
-
-<head>
-    <meta charset="UTF-8">
-    <title>阅读3.0源编辑器_V4.0</title>
-    <link rel="icon" href="../favicon.ico">
-    <link rel="stylesheet" type="text/css" href="index.css"/>
-</head>
-
-<body>
-<div class="editor">
-    <div class="setbox">
-        <div>
-            <a href="../index.html">←主页</a>
-            <b>书源</b>
-        </div>
-        <div class="rules">
-            <div><b>基本</b></div>
-            <div>
-                <div>源域名 :</div>
-                <textarea rows="1" id="bookSourceUrl" class="base" title="bookSourceUrl"
-                          placeholder="<必填>通常填写网站主页,例: https://www.qidian.com"></textarea>
-            </div>
-            <div>
-                <div>源类型 :</div>
-                <textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
-                          placeholder="&lt;必填&gt;0:文本 1:音频 2:图片 3:文件(只提供下载的网站)"></textarea>
-            </div>
-            <div>
-                <div>源名称 :</div>
-                <textarea rows="1" id="bookSourceName" class="base" title="bookSourceName"
-                          placeholder="&lt;必填&gt;会显示在源列表"></textarea>
-            </div>
-            <div>
-                <div>源分组 :</div>
-                <textarea rows="1" id="bookSourceGroup" class="base" title="bookSourceGroup"
-                          placeholder="&lt;选填&gt;描述源的特征信息"></textarea>
-            </div>
-            <div>
-                <div>源注释 :</div>
-                <textarea rows="1" id="bookSourceComment" class="base" title="bookSourceComment"
-                          placeholder="&lt;选填&gt;描述源作者和状态"></textarea>
-            </div>
-            <div>
-                <div>登录地址:</div>
-                <textarea rows="1" id="loginUrl" class="base" title="loginUrl"
-                          placeholder="&lt;选填&gt;填写网站登录网址,仅在需要登录的源有用"></textarea>
-            </div>
-            <div>
-                <div>登录界面:</div>
-                <textarea rows="3" id="loginUi" class="base" title="loginUi"
-                          placeholder="&lt;选填&gt;自定义登录界面"></textarea>
-            </div>
-            <div>
-                <div>登录检测:</div>
-                <textarea rows="3" id="loginCheckJs" class="base" title="loginCheckJs"
-                          placeholder="&lt;选填&gt;登录检测js"></textarea>
-            </div>
-            <div>
-                <div>并发率 :</div>
-                <textarea rows="1" id="concurrentRate" class="base" title="concurrentRate"
-                          placeholder="&lt;选填&gt;并发率"></textarea>
-            </div>
-            <div>
-                <div>请求头 :</div>
-                <textarea rows="3" id="header" class="base" title="header"
-                          placeholder="&lt;选填&gt;客户端标识"></textarea>
-            </div>
-            <div>
-                <div>链接验证:</div>
-                <textarea rows="1" id="bookUrlPattern" class="base" title="bookUrlPattern"
-                          placeholder="&lt;选填&gt;当详情页URL与源URL的域名不一致时有效,用于添加网址"></textarea>
-            </div>
-            <p></p>
-            <div><b>搜索</b></div>
-            <div>
-                <div>搜索地址:</div>
-                <textarea rows="1" id="searchUrl" class="base" title="searchUrl"
-                          placeholder="[域名可省略]/search.php@kw={{key}}"></textarea>
-            </div>
-            <div>
-                <div>校验文字:</div>
-                <textarea rows="1" id="ruleSearch_checkKeyWord" class="ruleSearch"
-                          title="checkKeyWord"
-                          placeholder="校验关键字"></textarea>
-            </div>
-            <div>
-                <div>列表规则:</div>
-                <textarea rows="1" id="ruleSearch_bookList" class="ruleSearch" title="bookList"
-                          placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
-            </div>
-            <div>
-                <div>书名规则:</div>
-                <textarea rows="1" id="ruleSearch_name" class="ruleSearch" title="name"
-                          placeholder="选择节点书名 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>作者规则:</div>
-                <textarea rows="1" id="ruleSearch_author" class="ruleSearch" title="author"
-                          placeholder="选择节点作者 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>分类规则:</div>
-                <textarea rows="1" id="ruleSearch_kind" class="ruleSearch" title="kind"
-                          placeholder="选择节点分类信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>字数规则:</div>
-                <textarea rows="1" id="ruleSearch_wordCount" class="ruleSearch" title="wordCount"
-                          placeholder="选择节点字数信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>最新章节:</div>
-                <textarea rows="1" id="ruleSearch_lastChapter" class="ruleSearch"
-                          title="lastChapter"
-                          placeholder="选择节点最新章节 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>简介规则:</div>
-                <textarea rows="1" id="ruleSearch_intro" class="ruleSearch" title="intro"
-                          placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>封面规则:</div>
-                <textarea rows="1" id="ruleSearch_coverUrl" class="ruleSearch" title="coverUrl"
-                          placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
-            </div>
-            <div>
-                <div>详情地址:</div>
-                <textarea rows="1" id="ruleSearch_bookUrl" class="ruleSearch" title="bookUrl"
-                          placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
-            </div>
-            <p></p>
-            <div><b>发现</b></div>
-            <div>
-                <div>发现地址:</div>
-                <textarea rows="6" id="exploreUrl" class="base" title="exploreUrl"
-                          placeholder="内容能显示在发现菜单&#10;每行一条发现分类(网址域名可省略),例:&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;..."></textarea>
-            </div>
-            <div>
-                <div>列表规则:</div>
-                <textarea rows="1" id="ruleExplore_bookList" class="ruleExplore" title="bookList"
-                          placeholder="选择书籍节点 (规则结果为List&lt;Element&gt;)"></textarea>
-            </div>
-            <div>
-                <div>书名规则:</div>
-                <textarea rows="1" id="ruleExplore_name" class="ruleExplore" title="name"
-                          placeholder="选择节点书名 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>作者规则:</div>
-                <textarea rows="1" id="ruleExplore_author" class="ruleExplore" title="author"
-                          placeholder="选择节点作者 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>分类规则:</div>
-                <textarea rows="1" id="ruleExplore_kind" class="ruleExplore" title="kind"
-                          placeholder="选择节点分类信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>字数规则:</div>
-                <textarea rows="1" id="ruleExplore_wordCount" class="ruleExplore" title="wordCount"
-                          placeholder="选择节点字数信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>最新章节:</div>
-                <textarea rows="1" id="ruleExplore_lastChapter" class="ruleExplore"
-                          title="lastChapter"
-                          placeholder="选择节点最新章节 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>简介规则:</div>
-                <textarea rows="1" id="ruleExplore_intro" class="ruleExplore" title="intro"
-                          placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>封面规则:</div>
-                <textarea rows="1" id="ruleExplore_coverUrl" class="ruleExplore" title="coverUrl"
-                          placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
-            </div>
-            <div>
-                <div>详情地址:</div>
-                <textarea rows="1" id="ruleExplore_bookUrl" class="ruleExplore" title="bookUrl"
-                          placeholder="选择书籍详情页网址 (规则结果为String类型的url)"></textarea>
-            </div>
-            <p></p>
-            <div><b>详情</b></div>
-            <div>
-                <div>预处理 :</div>
-                <textarea rows="3" id="ruleBookInfo_init" class="ruleBookInfo" title="init"
-                          placeholder="用于加速详情信息检索,只支持AllInOne规则"></textarea>
-            </div>
-            <div>
-                <div>书名规则:</div>
-                <textarea rows="1" id="ruleBookInfo_name" class="ruleBookInfo" title="name"
-                          placeholder="选择节点书名 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>作者规则:</div>
-                <textarea rows="1" id="ruleBookInfo_author" class="ruleBookInfo" title="author"
-                          placeholder="选择节点作者 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>分类规则:</div>
-                <textarea rows="1" id="ruleBookInfo_kind" class="ruleBookInfo" title="kind"
-                          placeholder="选择节点分类信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>字数规则:</div>
-                <textarea rows="1" id="ruleBookInfo_wordCount" class="ruleBookInfo"
-                          title="wordCount"
-                          placeholder="选择节点字数信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>最新章节:</div>
-                <textarea rows="1" id="ruleBookInfo_lastChapter" class="ruleBookInfo"
-                          title="lastChapter"
-                          placeholder="选择节点最新章节 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>简介规则:</div>
-                <textarea rows="1" id="ruleBookInfo_intro" class="ruleBookInfo" title="intro"
-                          placeholder="选择节点书籍简介 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>封面规则:</div>
-                <textarea rows="1" id="ruleBookInfo_coverUrl" class="ruleBookInfo" title="coverUrl"
-                          placeholder="选择节点书籍封面 (规则结果为String类型的url)"></textarea>
-            </div>
-            <div>
-                <div>目录地址:</div>
-                <textarea rows="1" id="ruleBookInfo_tocUrl" class="ruleBookInfo" title="tocUrl"
-                          placeholder="选择书籍详情页网址 (规则结果为String类型的url, 与详情页相同时可省略)"></textarea>
-            </div>
-            <p></p>
-            <div><b>目录</b></div>
-            <div>
-                <div>列表规则:</div>
-                <textarea rows="3" id="ruleToc_chapterList" class="ruleToc" title="chapterList"
-                          placeholder="选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)"></textarea>
-            </div>
-            <div>
-                <div>章节名称:</div>
-                <textarea rows="1" id="ruleToc_chapterName" class="ruleToc" title="chapterName"
-                          placeholder="选择章节名称 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>章节地址:</div>
-                <textarea rows="1" id="ruleToc_chapterUrl" class="ruleToc" title="chapterUrl"
-                          placeholder="选择章节链接 (规则结果为String类型的Url)"></textarea>
-            </div>
-            <div>
-                <div>卷名标识:</div>
-                <textarea rows="1" id="ruleToc_isVolume" class="ruleToc" title="isVolume"
-                          placeholder="章节名称是否是卷名 (规则结果为Bool)"></textarea>
-            </div>
-            <div>
-                <div>收费标识:</div>
-                <textarea rows="1" id="ruleToc_isVip" class="ruleToc" title="isVip"
-                          placeholder="章节是否为VIP章节 (规则结果为Bool)"></textarea>
-            </div>
-            <div>
-                <div>购买标识:</div>
-                <textarea rows="1" id="ruleToc_isPay" class="ruleToc" title="isPay"
-                          placeholder="章节是否为已购买 (规则结果为Bool)"></textarea>
-            </div>
-            <div>
-                <div>章节信息:</div>
-                <textarea rows="1" id="ruleToc_updateTime" class="ruleToc" title="updateTime"
-                          placeholder="选择章节信息 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>翻页规则:</div>
-                <textarea rows="1" id="ruleToc_nextTocUrl" class="ruleToc" title="nextTocUrl"
-                          placeholder="选择目录下一页链接 (规则结果为List&lt;String&gt;或String)"></textarea>
-            </div>
-            <p></p>
-            <div><b>正文</b></div>
-            <div>
-                <div>脚本注入:</div>
-                <textarea rows="3" id="ruleContent_webJs" class="ruleContent" title="webJs"
-                          placeholder="注入javascript,用于模拟鼠标点击等,必须有返回值,一般为String类型"></textarea>
-            </div>
-            <div>
-                <div>正文规则:</div>
-                <textarea rows="1" id="ruleContent_content" class="ruleContent" title="content"
-                          placeholder="选择正文内容 (规则结果为String)"></textarea>
-            </div>
-            <div>
-                <div>翻页规则:</div>
-                <textarea rows="1" id="ruleContent_nextContentUrl" class="ruleContent"
-                          title="nextContentUrl"
-                          placeholder="选择下一分页(不是下一章)链接 (规则结果为String类型的Url)"></textarea>
-            </div>
-            <div>
-                <div>资源正则:</div>
-                <textarea rows="1" id="ruleContent_sourceRegex" class="ruleContent"
-                          title="sourceRegex"
-                          placeholder="匹配资源的url特征,用于嗅探"></textarea>
-            </div>
-            <div>
-                <div>替换规则:</div>
-                <textarea rows="1" id="ruleContent_replaceRegex" class="ruleContent"
-                          title="replaceRegex"
-                          placeholder="多页内容合并后替换,用于正文净化"></textarea>
-            </div>
-            <div>
-                <div>图片样式:</div>
-                <textarea rows="1" id="ruleContent_imageStyle" class="ruleContent"
-                          title="imageStyle"
-                          placeholder="FULL:铺满 不填:默认样式"></textarea>
-            </div>
-            <div>
-                <div>购买操作:</div>
-                <textarea rows="1" id="ruleContent_payAction" class="ruleContent"
-                          title="payAction"
-                          placeholder="购买章节 返回链接或js"></textarea>
-            </div>
-            <p></p>
-            <div><b>其它规则</b></div>
-            <div>
-                <div>启用搜索:</div>
-                <textarea rows="1" id="enabled" class="base" title="enabled"
-                          placeholder="启用: true  关闭: false (可选,默认true)"></textarea>
-            </div>
-            <div>
-                <div>启用发现:</div>
-                <textarea rows="1" id="enabledExplore" class="base" title="enabledExplore"
-                          placeholder="启用: true  关闭: false (可选,默认true)"></textarea>
-            </div>
-            <div>
-                <div>搜索权重:</div>
-                <textarea rows="1" id="weight" class="base" title="weight"
-                          placeholder="整数: 0~N (可选,默认0) | 数字越大越靠前"></textarea>
-            </div>
-            <div>
-                <div>排序编号:</div>
-                <textarea rows="1" id="customOrder" class="base" title="customOrder"
-                          placeholder="整数: 0~N (可选,默认0) | 数字越小越靠前"></textarea>
-            </div>
-            <div style="display:none;">
-                <div>更新时间:</div>
-                <textarea rows="1" id="lastUpdateTime" class="base" title="lastUpdateTime"
-                          placeholder="毫秒级时间戳 (自动生成) | 请勿手动填写"></textarea>
-            </div>
-        </div>
-    </div>
-    <div class="menu">
-        <svg class="button">
-            <text x="50%" y="55%">⇈推送源</text>
-            <rect id="push"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⇊拉取源</text>
-            <rect id="pull"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⋘编辑源</text>
-            <rect id="editor"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⋙生成源</text>
-            <rect id="conver"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">✗清空表单</text>
-            <rect id="initial"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">↶撤销操作</text>
-            <rect id="undo"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">↷重做操作</text>
-            <rect id="redo"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">⇏调试源</text>
-            <rect id="debug"></rect>
-        </svg>
-        <svg class="button">
-            <text x="50%" y="55%">✓保存源</text>
-            <rect id="accept"></rect>
-        </svg>
-    </div>
-    <div class="outbox">
-        <div class="tabbox">
-            <div class="tabtitle">
-                <div name="编辑源" class="tab1 this">编辑源</div>
-                <div name="调试源" class="tab2">调试源</div>
-                <div name="源列表" class="tab3">源列表</div>
-                <div name="帮助信息" class="tab4">帮助信息</div>
-            </div>
-            <div class="tabbody">
-                <div class="tab1 this">
-                        <textarea class="context" id="RuleJsonString"
-                                  placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"></textarea>
-                </div>
-                <div class="tab2">
-                    <input type="text" class="inputbox" id="DebugKey" placeholder="输入搜索关键字,默认搜「我的」">
-                    <textarea class="context" id="DebugConsole" placeholder="这里用于输出调试信息"></textarea>
-                </div>
-                <div class="tab3">
-                    <input type="text" class="inputbox" id="Filter"
-                           placeholder="输入筛选关键词(源名称、源URL或源分组)后按回车筛选源">
-                    <div class="titlebar">
-                        <button id="Import">导入源文件</button>
-                        <button id="Export">导出源文件</button>
-                        <button id="Delete">删除选中源</button>
-                        <button id="ClrAll">清空列表</button>
-                    </div>
-                    <div class="context" id="RuleList"></div>
-                </div>
-                <div class="tab4">
-                    <div class="context link">
-                        <a target="_blank" href="https://alanskycn.gitee.io/teachme">源制作教程</a>
-                        <a target="_blank"
-                           href="https://zhuanlan.zhihu.com/p/29436838">Xpath基础教程</a>
-                        <a target="_blank"
-                           href="https://zhuanlan.zhihu.com/p/32187820">Xpath高级教程</a>
-                        <a target="_blank" href="https://www.w3cschool.cn/regex_rmjc">正则表达式教程</a>
-                        <a target="_blank" href="https://regexr.com">正则表达式在线验证工具</a>
-                        <div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义
-                            <br>(?s) 前缀表示跨行解析
-                            <br>(?m) 前缀表示逐行匹配
-                            <br>(?i) 前缀表示忽略大小写
-                        </div>
-                        <a target="_blank" href="https://www.beta.browxy.com">代码在线运行工具</a>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-<script type="text/javascript" src="index.js"></script>
-</body>
-
-</html>
+<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="./favicon.ico"><title>legado_web_editor</title><script defer="defer" src="./js/chunk-vendors.86212f98.js"></script><script defer="defer" src="./js/app.836bd8bb.js"></script><link href="./css/app.37e72839.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but legado_web_editor doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

+ 0 - 516
app/src/main/assets/web/bookSource/index.js

@@ -1,516 +0,0 @@
-// 简化js原生选择器
-function $(selector) { return document.querySelector(selector); }
-function $$(selector) { return document.querySelectorAll(selector); }
-// 读写Hash值(val未赋值时为读取)
-function hashParam(key, val) {
-	let hashstr = decodeURIComponent(window.location.hash);
-	let regKey = new RegExp(`${key}=([^&]*)`);
-	let getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;
-	if (val == undefined) return getVal;
-	if (hashstr == '' || hashstr == '#') {
-		window.location.hash = `#${key}=${val}`;
-	}
-	else {
-		if (getVal) window.location.hash = hashstr.replace(getVal, val);
-		else {
-			window.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`;
-		}
-	}
-}
-// 创建源规则容器对象
-function Container() {
-	let ruleJson = {};
-	let searchJson = {};
-	let exploreJson = {};
-	let bookInfoJson = {};
-	let tocJson = {};
-	let contentJson = {};
-
-	// 基本以及其他
-	$$('.rules .base').forEach(item => ruleJson[item.title] = '');
-	ruleJson.lastUpdateTime = 0;
-	ruleJson.customOrder = 0;
-	ruleJson.weight = 0;
-	ruleJson.enabled = true;
-	ruleJson.enabledExplore = true;
-
-	// 搜索规则
-	$$('.rules .ruleSearch').forEach(item => searchJson[item.title] = '');
-	ruleJson.ruleSearch = searchJson;
-
-	// 发现规则
-	$$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = '');
-	ruleJson.ruleExplore = exploreJson;
-
-	// 详情页规则
-	$$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = '');
-	ruleJson.ruleBookInfo = bookInfoJson;
-
-	// 目录规则
-	$$('.rules .ruleToc').forEach(item => tocJson[item.title] = '');
-	ruleJson.ruleToc = tocJson;
-
-	// 正文规则
-	$$('.rules .ruleContent').forEach(item => contentJson[item.title] = '');
-	ruleJson.ruleContent = contentJson;
-
-	return ruleJson;
-}
-// 选项卡Tab切换事件处理
-function showTab(tabName) {
-	$$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });
-	$$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); });
-	$(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this';
-	$(`.tabtitle>*[name=${tabName}]`).className += ' this';
-	hashParam('tab', tabName);
-}
-// 源列表列表标签构造函数
-function newRule(rule) {
-	return `<label for="${rule.bookSourceUrl}"><input type="radio" name="rule" id="${rule.bookSourceUrl}"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`;
-}
-// 缓存规则列表
-var RuleSources = [];
-if (localStorage.getItem('BookSources')) {
-	RuleSources = JSON.parse(localStorage.getItem('BookSources'));
-	RuleSources.forEach(item => $('#RuleList').innerHTML += newRule(item));
-}
-// 页面加载完成事件
-window.onload = () => {
-	$$('.tabtitle>*').forEach(item => {
-		item.addEventListener('click', () => {
-			showTab(item.innerHTML);
-		});
-	});
-	if (hashParam('tab')) showTab(hashParam('tab'));
-}
-// 获取数据
-function HttpGet(url) {
-	return fetch(hashParam('domain') ? hashParam('domain') + url : url)
-		.then(res => res.json()).catch(err => console.error('Error:', err));
-}
-// 提交数据
-function HttpPost(url, data) {
-	return fetch(hashParam('domain') ? hashParam('domain') + url : url, {
-		body: JSON.stringify(data),
-		method: 'POST',
-		mode: "cors",
-		headers: new Headers({
-			'Content-Type': 'application/json;charset=utf-8'
-		})
-	}).then(res => res.json()).catch(err => console.error('Error:', err));
-}
-// 将源表单转化为源对象
-function rule2json() {
-	let RuleJSON = Container();
-	// 转换base
-	Object.keys(RuleJSON).forEach(key => {
-		if (!key.startsWith("rule")) {
-			RuleJSON[key] = $('#' + key).value;
-		}
-	});
-
-	// 转换搜索规则
-	let searchJson = {};
-	Object.keys(RuleJSON.ruleSearch).forEach(key => {
-		if ($('#' + 'ruleSearch_' + key).value)
-			searchJson[key] = $('#' + 'ruleSearch_' + key).value;
-	});
-	RuleJSON.ruleSearch = searchJson;
-
-	// 转换发现规则
-	let exploreJson = {};
-	Object.keys(RuleJSON.ruleExplore).forEach(key => {
-		if ($('#' + 'ruleExplore_' + key).value)
-			exploreJson[key] = $('#' + 'ruleExplore_' + key).value;
-	});
-	RuleJSON.ruleExplore = exploreJson;
-
-	// 转换详情页规则
-	let bookInfoJson = {};
-	Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
-		if ($('#' + 'ruleBookInfo_' + key).value)
-			bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value;
-	});
-	RuleJSON.ruleBookInfo = bookInfoJson;
-
-	// 转换目录规则
-	let tocJson = {};
-	Object.keys(RuleJSON.ruleToc).forEach(key => {
-		if ($('#' + 'ruleToc_' + key).value)
-			tocJson[key] = $('#' + 'ruleToc_' + key).value;
-	});
-	RuleJSON.ruleToc = tocJson;
-
-	// 转换正文规则
-	let contentJson = {};
-	Object.keys(RuleJSON.ruleContent).forEach(key => {
-		if ($('#' + 'ruleContent_' + key).value)
-			contentJson[key] = $('#' + 'ruleContent_' + key).value;
-	});
-	RuleJSON.ruleContent = contentJson;
-
-	RuleJSON.lastUpdateTime = new Date().getTime();
-	RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder);
-	RuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);
-	RuleJSON.bookSourceType == RuleJSON.bookSourceType == '' ? 0 : parseInt(RuleJSON.bookSourceType);
-	RuleJSON.enabled = RuleJSON.enabled == '' || String(RuleJSON.enabled).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
-	RuleJSON.enabledExplore = RuleJSON.enabledExplore == '' || String(RuleJSON.enabledExplore).toLocaleLowerCase().replace(/^\s*|\s*$/g, '') == 'true';
-	return RuleJSON;
-}
-// 将源对象填充到源表单
-function json2rule(RuleEditor) {
-	let RuleJSON = Container();
-	// 转换base
-	Object.keys(RuleJSON).forEach(key => {
-		if (!key.startsWith("rule")) {
-			let val = RuleEditor[key];
-			if (typeof val == "number") {
-				$("#" + key).value = val ? String(val) : '0';
-			}
-			else if (typeof val == "boolean") {
-				$("#" + key).value = val ? String(val) : 'false';
-			}
-			else {
-				$("#" + key).value = val ? String(val) : '';
-			}
-		}
-	});
-
-	// 转换搜索规则
-	if (RuleEditor.ruleSearch) {
-		let searchJson = RuleEditor.ruleSearch;
-		Object.keys(RuleJSON.ruleSearch).forEach(key => {
-			$('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : '';
-		});
-	}
-
-	// 转换发现规则
-	if (RuleEditor.ruleExplore) {
-		let exploreJson = RuleEditor.ruleExplore;
-		Object.keys(RuleJSON.ruleExplore).forEach(key => {
-			$('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : '';
-		});
-	}
-
-	// 转换详情页规则
-	if (RuleEditor.ruleBookInfo) {
-		let bookInfoJson = RuleEditor.ruleBookInfo;
-        Object.keys(RuleJSON.ruleBookInfo).forEach(key => {
-			$('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : '';
-		});
-	}
-
-	// 转换目录规则
-	if (RuleEditor.ruleToc) {
-		let tocJson = RuleEditor.ruleToc;
-		Object.keys(RuleJSON.ruleToc).forEach(key => {
-			$('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : '';
-		});
-	}
-
-	// 转换正文规则
-	if (RuleEditor.ruleContent) {
-		let contentJson = RuleEditor.ruleContent;
-        Object.keys(RuleJSON.ruleContent).forEach(key => {
-			$('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : '';
-		});
-	}
-}
-// 记录操作过程
-var course = { "old": [], "now": {}, "new": [] };
-if (localStorage.getItem('bookSourceCourse')) {
-	course = JSON.parse(localStorage.getItem('bookSourceCourse'));
-	json2rule(course.now);
-}
-else {
-	course.now = rule2json();
-	window.localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-}
-function todo() {
-	course.old.push(Object.assign({}, course.now));
-	course.now = rule2json();
-	course.new = [];
-	if (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小
-	localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-}
-function undo() {
-	course = JSON.parse(localStorage.getItem('bookSourceCourse'));
-	if (course.old.length > 0) {
-		course.new.push(course.now);
-		course.now = course.old.pop();
-		localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-		json2rule(course.now);
-	}
-}
-function redo() {
-	course = JSON.parse(localStorage.getItem('bookSourceCourse'));
-	if (course.new.length > 0) {
-		course.old.push(course.now);
-		course.now = course.new.pop();
-		localStorage.setItem('bookSourceCourse', JSON.stringify(course));
-		json2rule(course.now);
-	}
-}
-function setRule(editRule) {
-	let checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl);
-	if ($(`input[id="${editRule.bookSourceUrl}"]`)) {
-		Object.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; });
-		$(`input[id="${editRule.bookSourceUrl}"]+*`).innerHTML = `${editRule.bookSourceName}<br>${editRule.bookSourceUrl}`;
-	} else {
-		RuleSources.push(editRule);
-		$('#RuleList').innerHTML += newRule(editRule);
-	}
-}
-$$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) });
-$$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) });
-// 处理按钮点击事件
-$('.menu').addEventListener('click', e => {
-	let thisNode = e.target;
-	thisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') :
-		thisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null;
-	if (!thisNode) return;
-	if (thisNode.getAttribute('class') == 'busy') return;
-	thisNode.setAttribute('class', 'busy');
-	switch (thisNode.id) {
-		case 'push':
-			$$('#RuleList>label>div').forEach(item => { item.className = ''; });
-			(async () => {
-				await HttpPost(`/saveBookSources`, RuleSources).then(json => {
-					if (json.isSuccess) {
-						let okData = json.data;
-						if (Array.isArray(okData)) {
-							let failMsg = ``;
-							if (RuleSources.length > okData.length) {
-								RuleSources.forEach(item => {
-									if (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { }
-									else { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; }
-								});
-								failMsg = '\n推送失败的源将用红色字体标注!';
-							}
-							alert(`批量推送源到「阅读3.0APP」\n共计: ${RuleSources.length} 条\n成功: ${okData.length} 条\n失败: ${RuleSources.length - okData.length} 条${failMsg}`);
-						}
-						else {
-							alert(`批量推送源到「阅读3.0APP」成功!\n共计: ${RuleSources.length} 条`);
-						}
-					}
-					else {
-						alert(`批量推送源失败!\nErrorMsg: ${json.errorMsg}`);
-					}
-				}).catch(err => { alert(`批量推送源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-				thisNode.setAttribute('class', '');
-			})();
-			return;
-		case 'pull':
-			showTab('源列表');
-			(async () => {
-				await HttpGet(`/getBookSources`).then(json => {
-					if (json.isSuccess) {
-						$('#RuleList').innerHTML = ''
-						localStorage.setItem('BookSources', JSON.stringify(RuleSources = json.data));
-						RuleSources.forEach(item => {
-							$('#RuleList').innerHTML += newRule(item);
-						});
-						alert(`成功拉取 ${RuleSources.length} 条源`);
-					}
-					else {
-						alert(`批量拉取源失败!\nErrorMsg: ${json.errorMsg}`);
-					}
-				}).catch(err => { alert(`批量拉取源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-				thisNode.setAttribute('class', '');
-			})();
-			return;
-		case 'editor':
-			if ($('#RuleJsonString').value == '') break;
-			try {
-				json2rule(JSON.parse($('#RuleJsonString').value));
-				todo();
-			} catch (error) {
-				console.log(error);
-				alert(error);
-			}
-			break;
-		case 'conver':
-			showTab('编辑源');
-			$('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4);
-			break;
-		case 'initial':
-			$$('.rules textarea').forEach(item => { item.value = '' });
-			todo();
-			break;
-		case 'undo':
-			undo()
-			break;
-		case 'redo':
-			redo()
-			break;
-		case 'debug':
-			showTab('调试源');
-			let wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\d+$/, (port) => (parseInt(port) + 1));
-			let DebugInfos = $('#DebugConsole');
-			function DebugPrint(msg) { DebugInfos.value += `\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }
-			let saveRule = [rule2json()];
-			HttpPost(`/saveBookSources`, saveRule).then(sResult => {
-				if (sResult.isSuccess) {
-					let sKey = DebugKey.value ? DebugKey.value : '我的';
-					$('#DebugConsole').value = `源《${saveRule[0].bookSourceName}》保存成功!使用搜索关键字“${sKey}”开始调试...`;
-					let ws = new WebSocket(`${wsOrigin}/bookSourceDebug`);
-					ws.onopen = () => {
-						ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`);
-					};
-					ws.onmessage = (msg) => {
-					    console.log('[调试]', msg);
-						DebugPrint(msg.data);
-					};
-					ws.onerror = (err) => {
-						throw `${err.data}`;
-					}
-					ws.onclose = () => {
-						thisNode.setAttribute('class', '');
-						DebugPrint(`\n调试服务已关闭!`);
-					}
-				} else throw `${sResult.errorMsg}`;
-			}).catch(err => {
-				DebugPrint(`调试过程意外中止,以下是详细错误信息:\n${err}`);
-				thisNode.setAttribute('class', '');
-			});
-			return;
-		case 'accept':
-			(async () => {
-				let saveRule = [rule2json()];
-				await HttpPost(`/saveBookSource`, saveRule[0]).then(json => {
-					alert(json.isSuccess ? `源《${saveRule[0].bookSourceName}》已成功保存到「阅读3.0APP」` : `源《${saveRule[0].bookSourceName}》保存失败!\nErrorMsg: ${json.errorMsg}`);
-					setRule(saveRule[0]);
-				}).catch(err => { alert(`保存源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-				thisNode.setAttribute('class', '');
-			})();
-			return;
-		default:
-	}
-	setTimeout(() => { thisNode.setAttribute('class', ''); }, 500);
-});
-$('#DebugKey').addEventListener('keydown', e => {
-	if (e.keyCode == 13) {
-		let clickEvent = document.createEvent('MouseEvents');
-		clickEvent.initEvent("click", true, false);
-		$('#debug').dispatchEvent(clickEvent);
-	}
-});
-$('#Filter').addEventListener('keydown', e => {
-	if (e.keyCode == 13) {
-		let cashList = [];
-		$('#RuleList').innerHTML = "";
-		let sKey = Filter.value ? Filter.value : '';
-		if (sKey == '') {
-			cashList = RuleSources;
-		} else {
-			let patt = new RegExp(sKey);
-			RuleSources.forEach(source => {
-				if (patt.test(source.bookSourceUrl) || patt.test(source.bookSourceName) || patt.test(source.bookSourceGroup)) {
-					cashList.push(source);
-				}
-			})
-		}
-		cashList.forEach(source => {
-			$('#RuleList').innerHTML += newRule(source);
-		})
-	}
-});
-
-// 列表规则更改事件
-$('#RuleList').addEventListener('click', e => {
-	let editRule = null;
-	if (e.target && e.target.getAttribute('name') == 'rule') {
-		editRule = rule2json();
-		json2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id));
-	} else return;
-	if (editRule.bookSourceUrl == '') return;
-	if (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\/\/|\/.*/g, '');
-	setRule(editRule);
-	localStorage.setItem('BookSources', JSON.stringify(RuleSources));
-});
-// 处理列表按钮事件
-$('.tab3>.titlebar').addEventListener('click', e => {
-	let thisNode = e.target;
-	if (thisNode.nodeName != 'BUTTON') return;
-	switch (thisNode.id) {
-		case 'Import':
-			let fileImport = document.createElement('input');
-			fileImport.type = 'file';
-			fileImport.accept = '.json';
-			fileImport.addEventListener('change', () => {
-				let file = fileImport.files[0];
-				let reader = new FileReader();
-				reader.onloadend = function (evt) {
-					if (evt.target.readyState == FileReader.DONE) {
-						let fileText = evt.target.result;
-						try {
-							let fileJson = JSON.parse(fileText);
-							let newSources = [];
-							newSources.push(...fileJson);
-							if (window.confirm(`如何处理导入的源?\n"确定": 覆盖当前列表(不会删除APP源)\n"取消": 插入列表尾部(自动忽略重复源)`)) {
-								localStorage.setItem('BookSources', JSON.stringify(RuleSources = newSources));
-								$('#RuleList').innerHTML = ''
-								RuleSources.forEach(item => {
-									$('#RuleList').innerHTML += newRule(item);
-								});
-							}
-							else {
-								newSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));
-								RuleSources.push(...newSources);
-								localStorage.setItem('BookSources', JSON.stringify(RuleSources));
-								newSources.forEach(item => {
-									$('#RuleList').innerHTML += newRule(item);
-								});
-							}
-							alert(`成功导入 ${newSources.length} 条源`);
-						}
-						catch (err) {
-							alert(`导入源文件失败!\n${err}`);
-						}
-					}
-				};
-				reader.readAsText(file);
-			}, false);
-			fileImport.click();
-			break;
-		case 'Export':
-			let fileExport = document.createElement('a');
-			fileExport.download = `Rules${Date().replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, '$2$1$3').replace(/:/g, '')}.json`;
-			let myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: "application/json" });
-			fileExport.href = window.URL.createObjectURL(myBlob);
-			fileExport.click();
-			break;
-		case 'Delete':
-			let selectRule = $('#RuleList input:checked');
-			if (!selectRule) {
-				alert(`没有源被选中!`);
-				return;
-			}
-			if (confirm(`确定要删除选定源吗?\n(同时删除APP内源)`)) {
-				let selectRuleUrl = selectRule.id;
-				let deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的源
-				let laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl));  // 提取待留下的源
-				HttpPost(`/deleteBookSources`, deleteSources).then(json => {
-					if (json.isSuccess) {
-						let selectNode = document.getElementById(selectRuleUrl).parentNode;
-						selectNode.parentNode.removeChild(selectNode);
-						localStorage.setItem('BookSources', JSON.stringify(RuleSources = laveSources));
-						if ($('#bookSourceUrl').value == selectRuleUrl) {
-							$$('.rules textarea').forEach(item => { item.value = '' });
-							todo();
-						}
-						console.log(deleteSources);
-						console.log(`以上源已删除!`)
-					}
-				}).catch(err => { alert(`删除源失败,无法连接到「阅读3.0APP」!\n${err}`); });
-			}
-			break;
-		case 'ClrAll':
-			if (confirm(`确定要清空当前源列表吗?\n(不会删除APP内源)`)) {
-				localStorage.setItem('BookSources', JSON.stringify(RuleSources = []));
-				$('#RuleList').innerHTML = ''
-			}
-			break;
-		default:
-	}
-});

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.2c3e42a8.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.2c3e42a8.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.836bd8bb.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/js/app.836bd8bb.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/src/main/assets/web/bookSource/js/chunk-vendors.86212f98.js.map


+ 1 - 1
app/src/main/java/io/legado/app/api/controller/BookController.kt

@@ -82,7 +82,7 @@ object BookController {
         this.bookUrl = bookUrl
         this.bookUrl = bookUrl
         val bitmap = runBlocking {
         val bitmap = runBlocking {
             ImageProvider.cacheImage(book, src, bookSource)
             ImageProvider.cacheImage(book, src, bookSource)
-            ImageProvider.getImage(book, src, width)
+            ImageProvider.getImage(book, src, width)!!
         }
         }
         return returnData.setData(bitmap)
         return returnData.setData(bitmap)
     }
     }

+ 14 - 4
app/src/main/java/io/legado/app/help/AppWebDav.kt

@@ -17,6 +17,7 @@ import io.legado.app.lib.webdav.Authorization
 import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.lib.webdav.WebDavFile
 import io.legado.app.lib.webdav.WebDavFile
+import io.legado.app.ui.widget.dialog.WaitDialog
 import io.legado.app.utils.*
 import io.legado.app.utils.*
 import kotlinx.coroutines.*
 import kotlinx.coroutines.*
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
@@ -109,10 +110,19 @@ object AppWebDav {
                     items = names
                     items = names
                 ) { _, index ->
                 ) { _, index ->
                     if (index in 0 until names.size) {
                     if (index in 0 until names.size) {
-                        Coroutine.async {
+                        val waitDialog = WaitDialog(context)
+                        waitDialog.setText("恢复中…")
+                        waitDialog.show()
+                        val task = Coroutine.async {
                             restoreWebDav(names[index])
                             restoreWebDav(names[index])
                         }.onError {
                         }.onError {
+                            AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it)
                             appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}")
                             appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}")
+                        }.onFinally(Main) {
+                            waitDialog.dismiss()
+                        }
+                        waitDialog.setOnCancelListener {
+                            task.cancel()
                         }
                         }
                     }
                     }
                 }
                 }
@@ -186,7 +196,7 @@ object AppWebDav {
             }
             }
         } catch (e: Exception) {
         } catch (e: Exception) {
             val msg = "WebDav导出\n${e.localizedMessage}"
             val msg = "WebDav导出\n${e.localizedMessage}"
-            AppLog.put(msg)
+            AppLog.put(msg, e)
             appCtx.toastOnUi(msg)
             appCtx.toastOnUi(msg)
         }
         }
     }
     }
@@ -201,7 +211,7 @@ object AppWebDav {
             val url = getProgressUrl(book.name, book.author)
             val url = getProgressUrl(book.name, book.author)
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
         }.onError {
         }.onError {
-            AppLog.put("上传进度失败\n${it.localizedMessage}")
+            AppLog.put("上传进度失败\n${it.localizedMessage}", it)
         }
         }
     }
     }
 
 
@@ -214,7 +224,7 @@ object AppWebDav {
             val url = getProgressUrl(bookProgress.name, bookProgress.author)
             val url = getProgressUrl(bookProgress.name, bookProgress.author)
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
         }.onError {
         }.onError {
-            AppLog.put("上传进度失败\n${it.localizedMessage}")
+            AppLog.put("上传进度失败\n${it.localizedMessage}", it)
         }
         }
     }
     }
 
 

+ 19 - 8
app/src/main/java/io/legado/app/help/BookHelp.kt

@@ -31,6 +31,7 @@ object BookHelp {
     private val downloadDir: File = appCtx.externalFiles
     private val downloadDir: File = appCtx.externalFiles
     private const val cacheFolderName = "book_cache"
     private const val cacheFolderName = "book_cache"
     private const val cacheImageFolderName = "images"
     private const val cacheImageFolderName = "images"
+    private const val cacheEpubFolderName = "epub"
     private val downloadImages = CopyOnWriteArraySet<String>()
     private val downloadImages = CopyOnWriteArraySet<String>()
 
 
     fun clearCache() {
     fun clearCache() {
@@ -55,15 +56,24 @@ object BookHelp {
      */
      */
     suspend fun clearInvalidCache() {
     suspend fun clearInvalidCache() {
         withContext(IO) {
         withContext(IO) {
-            val bookFolderNames = appDb.bookDao.all.map {
-                it.getFolderName()
+            val bookFolderNames = ArrayList<String>()
+            val originNames = ArrayList<String>()
+            appDb.bookDao.all.forEach {
+                bookFolderNames.add(it.getFolderName())
+                if (it.isEpub()) originNames.add(it.originName)
             }
             }
-            val file = downloadDir.getFile(cacheFolderName)
-            file.listFiles()?.forEach { bookFile ->
-                if (!bookFolderNames.contains(bookFile.name)) {
-                    FileUtils.delete(bookFile.absolutePath)
+            downloadDir.getFile(cacheFolderName)
+                .listFiles()?.forEach { bookFile ->
+                    if (!bookFolderNames.contains(bookFile.name)) {
+                        FileUtils.delete(bookFile.absolutePath)
+                    }
+                }
+            downloadDir.getFile(cacheEpubFolderName)
+                .listFiles()?.forEach { epubFile ->
+                    if (!originNames.contains(epubFile.name)) {
+                        FileUtils.delete(epubFile.absolutePath)
+                    }
                 }
                 }
-            }
         }
         }
     }
     }
 
 
@@ -165,7 +175,8 @@ object BookHelp {
     fun getEpubFile(book: Book): ZipFile {
     fun getEpubFile(book: Book): ZipFile {
         val uri = Uri.parse(book.bookUrl)
         val uri = Uri.parse(book.bookUrl)
         if (uri.isContentScheme()) {
         if (uri.isContentScheme()) {
-            val path = FileUtils.getPath(downloadDir, cacheFolderName, book.getFolderName(), book.originName)
+            FileUtils.createFolderIfNotExist(downloadDir, cacheEpubFolderName)
+            val path = FileUtils.getPath(downloadDir, cacheEpubFolderName, book.originName)
             val file = File(path)
             val file = File(path)
             val doc = DocumentFile.fromSingleUri(appCtx, uri)
             val doc = DocumentFile.fromSingleUri(appCtx, uri)
                 ?: throw IOException("文件不存在")
                 ?: throw IOException("文件不存在")

+ 3 - 1
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -55,7 +55,7 @@ interface JsExtensions {
                 val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())
                 val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())
                 analyzeUrl.getStrResponseAwait().body
                 analyzeUrl.getStrResponseAwait().body
             }.onFailure {
             }.onFailure {
-                log("ajax(${urlStr}) error\n${it.stackTraceToString()}")
+                AppLog.put("ajax(${urlStr}) error\n${it.localizedMessage}", it)
                 it.printOnDebug()
                 it.printOnDebug()
             }.getOrElse {
             }.getOrElse {
                 it.msg
                 it.msg
@@ -1008,6 +1008,7 @@ interface JsExtensions {
      * @param key 密钥
      * @param key 密钥
      * @return 16进制字符串
      * @return 16进制字符串
      */
      */
+    @Suppress("FunctionName")
     fun HMacHex(
     fun HMacHex(
         data: String,
         data: String,
         algorithm: String,
         algorithm: String,
@@ -1024,6 +1025,7 @@ interface JsExtensions {
      * @param key 密钥
      * @param key 密钥
      * @return Base64字符串
      * @return Base64字符串
      */
      */
+    @Suppress("FunctionName")
     fun HMacBase64(
     fun HMacBase64(
         data: String,
         data: String,
         algorithm: String,
         algorithm: String,

+ 5 - 3
app/src/main/java/io/legado/app/lib/webdav/WebDav.kt

@@ -232,7 +232,7 @@ open class WebDav(val path: String, val authorization: Authorization) {
                 }
                 }
             }
             }
         }.onFailure {
         }.onFailure {
-            AppLog.put("WebDav创建目录失败\n${it.localizedMessage}")
+            AppLog.put("WebDav创建目录失败\n${it.localizedMessage}", it)
         }.isSuccess
         }.isSuccess
     }
     }
 
 
@@ -286,6 +286,7 @@ open class WebDav(val path: String, val authorization: Authorization) {
                 checkResult(it)
                 checkResult(it)
             }
             }
         }.onFailure {
         }.onFailure {
+            AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
         }
         }
     }
     }
@@ -303,12 +304,13 @@ open class WebDav(val path: String, val authorization: Authorization) {
                 checkResult(it)
                 checkResult(it)
             }
             }
         }.onFailure {
         }.onFailure {
+            AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
             throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
         }
         }
     }
     }
 
 
     @Throws(WebDavException::class)
     @Throws(WebDavException::class)
-    private suspend fun downloadInputStream(): InputStream {
+    suspend fun downloadInputStream(): InputStream {
         val url = httpUrl ?: throw WebDavException("WebDav下载出错\nurl为空")
         val url = httpUrl ?: throw WebDavException("WebDav下载出错\nurl为空")
         val byteStream = webDavClient.newCallResponse {
         val byteStream = webDavClient.newCallResponse {
             url(url)
             url(url)
@@ -332,7 +334,7 @@ open class WebDav(val path: String, val authorization: Authorization) {
                 checkResult(it)
                 checkResult(it)
             }
             }
         }.onFailure {
         }.onFailure {
-            AppLog.put("WebDav删除失败\n${it.localizedMessage}")
+            AppLog.put("WebDav删除失败\n${it.localizedMessage}", it)
         }.isSuccess
         }.isSuccess
     }
     }
 
 

+ 48 - 0
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt

@@ -23,6 +23,8 @@ import kotlinx.coroutines.runBlocking
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.Response
 import okhttp3.Response
+import java.io.ByteArrayInputStream
+import java.io.InputStream
 import java.net.URLEncoder
 import java.net.URLEncoder
 import java.util.regex.Pattern
 import java.util.regex.Pattern
 
 
@@ -497,6 +499,52 @@ class AnalyzeUrl(
         }
         }
     }
     }
 
 
+    /**
+     * 访问网站,返回InputStream
+     */
+    suspend fun getInputStreamAwait(): InputStream {
+        val concurrentRecord = fetchStart()
+
+        @Suppress("RegExpRedundantEscape")
+        val dataUriFindResult = dataUriRegex.find(urlNoQuery)
+        @Suppress("BlockingMethodInNonBlockingContext")
+        if (dataUriFindResult != null) {
+            val dataUriBase64 = dataUriFindResult.groupValues[1]
+            val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
+            fetchEnd(concurrentRecord)
+            return ByteArrayInputStream(byteArray)
+        } else {
+            setCookie(source?.getKey())
+            val inputStream = getProxyClient(proxy).newCallResponseBody(retry) {
+                addHeaders(headerMap)
+                when (method) {
+                    RequestMethod.POST -> {
+                        url(urlNoQuery)
+                        val contentType = headerMap["Content-Type"]
+                        val body = body
+                        if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
+                            postForm(fieldMap, true)
+                        } else if (!contentType.isNullOrBlank()) {
+                            val requestBody = body.toRequestBody(contentType.toMediaType())
+                            post(requestBody)
+                        } else {
+                            postJson(body)
+                        }
+                    }
+                    else -> get(urlNoQuery, fieldMap, true)
+                }
+            }.byteStream()
+            fetchEnd(concurrentRecord)
+            return inputStream
+        }
+    }
+
+    fun getInputStream(): InputStream {
+        return runBlocking {
+            getInputStreamAwait()
+        }
+    }
+
     /**
     /**
      * 上传文件
      * 上传文件
      */
      */

+ 9 - 9
app/src/main/java/io/legado/app/model/localBook/EpubFile.kt

@@ -1,7 +1,9 @@
 package io.legado.app.model.localBook
 package io.legado.app.model.localBook
 
 
 import android.graphics.Bitmap
 import android.graphics.Bitmap
+import android.graphics.BitmapFactory
 import android.text.TextUtils
 import android.text.TextUtils
+import io.legado.app.constant.AppLog
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.help.BookHelp
 import io.legado.app.help.BookHelp
@@ -85,17 +87,16 @@ class EpubFile(var book: Book) {
                 if (!File(book.coverUrl!!).exists()) {
                 if (!File(book.coverUrl!!).exists()) {
                     /*部分书籍DRM处理后,封面获取异常,待优化*/
                     /*部分书籍DRM处理后,封面获取异常,待优化*/
                     it.coverImage?.inputStream?.use { input ->
                     it.coverImage?.inputStream?.use { input ->
-                        BitmapUtils.decodeBitmap(input)?.let { cover ->
-                            val out =
-                                FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
-                            cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
-                            out.flush()
-                            out.close()
-                        }
+                        val cover = BitmapFactory.decodeStream(input)
+                        val out = FileOutputStream(FileUtils.createFileIfNotExist(book.coverUrl!!))
+                        cover.compress(Bitmap.CompressFormat.JPEG, 90, out)
+                        out.flush()
+                        out.close()
                     }
                     }
                 }
                 }
             }
             }
         } catch (e: Exception) {
         } catch (e: Exception) {
+            AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e)
             e.printOnDebug()
             e.printOnDebug()
         }
         }
     }
     }
@@ -109,6 +110,7 @@ class EpubFile(var book: Book) {
             val zipFile = BookHelp.getEpubFile(book)
             val zipFile = BookHelp.getEpubFile(book)
             EpubReader().readEpubLazy(zipFile, "utf-8")
             EpubReader().readEpubLazy(zipFile, "utf-8")
         }.onFailure {
         }.onFailure {
+            AppLog.put("读取Epub文件失败\n${it.localizedMessage}", it)
             it.printOnDebug()
             it.printOnDebug()
         }.getOrNull()
         }.getOrNull()
     }
     }
@@ -262,8 +264,6 @@ class EpubFile(var book: Book) {
                 }
                 }
             }
             }
         }
         }
-        book.latestChapterTitle = chapterList.lastOrNull()?.title
-        book.totalChapterNum = chapterList.size
         return chapterList
         return chapterList
     }
     }
 
 

+ 11 - 8
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -78,9 +78,12 @@ object LocalBook {
         if (chapters.isEmpty()) {
         if (chapters.isEmpty()) {
             throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
             throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
         }
         }
-        val lh = LinkedHashSet(chapters)
-        lh.forEachIndexed { index, bookChapter -> bookChapter.index = index }
-        return ArrayList(lh)
+        val list = ArrayList(LinkedHashSet(chapters))
+        list.forEachIndexed { index, bookChapter -> bookChapter.index = index }
+        book.latestChapterTitle = list.last().title
+        book.totalChapterNum = list.size
+        book.save()
+        return list
     }
     }
 
 
     fun getContent(book: Book, chapter: BookChapter): String? {
     fun getContent(book: Book, chapter: BookChapter): String? {
@@ -232,8 +235,8 @@ object LocalBook {
         AppConfig.defaultBookTreeUri
         AppConfig.defaultBookTreeUri
             ?: throw NoStackTraceException("没有设置书籍保存位置!")
             ?: throw NoStackTraceException("没有设置书籍保存位置!")
         val bytes = when {
         val bytes = when {
-            str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
-            str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
+            str.isAbsUrl() -> AnalyzeUrl(str, source = source).getInputStream()
+            str.isDataUrl() -> ByteArrayInputStream(Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT))
             else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
             else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
         }
         }
         return saveBookFile(bytes, fileName)
         return saveBookFile(bytes, fileName)
@@ -254,7 +257,7 @@ object LocalBook {
     }
     }
 
 
     fun saveBookFile(
     fun saveBookFile(
-        bytes: ByteArray,
+        inputStream: InputStream,
         fileName: String
         fileName: String
     ): Uri {
     ): Uri {
         val defaultBookTreeUri = AppConfig.defaultBookTreeUri
         val defaultBookTreeUri = AppConfig.defaultBookTreeUri
@@ -268,14 +271,14 @@ object LocalBook {
                     ?: throw SecurityException("Permission Denial")
                     ?: throw SecurityException("Permission Denial")
             }
             }
             appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
             appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
-                oStream.write(bytes)
+                inputStream.copyTo(oStream)
             }
             }
             doc.uri
             doc.uri
         } else {
         } else {
             val treeFile = File(treeUri.path!!)
             val treeFile = File(treeUri.path!!)
             val file = treeFile.getFile(fileName)
             val file = treeFile.getFile(fileName)
             FileOutputStream(file).use { oStream ->
             FileOutputStream(file).use { oStream ->
-                oStream.write(bytes)
+                inputStream.copyTo(oStream)
             }
             }
             Uri.fromFile(file)
             Uri.fromFile(file)
         }
         }

+ 0 - 3
app/src/main/java/io/legado/app/model/localBook/TextFile.kt

@@ -77,9 +77,6 @@ class TextFile(private val book: Book) {
             bookChapter.bookUrl = book.bookUrl
             bookChapter.bookUrl = book.bookUrl
             bookChapter.url = MD5Utils.md5Encode16(book.originName + index + bookChapter.title)
             bookChapter.url = MD5Utils.md5Encode16(book.originName + index + bookChapter.title)
         }
         }
-        book.latestChapterTitle = toc.last().title
-        book.totalChapterNum = toc.size
-        book.save()
         return toc
         return toc
     }
     }
 
 

+ 0 - 2
app/src/main/java/io/legado/app/model/localBook/UmdFile.kt

@@ -112,8 +112,6 @@ class UmdFile(var book: Book) {
             DebugLog.d(javaClass.name, chapter.url)
             DebugLog.d(javaClass.name, chapter.url)
             chapterList.add(chapter)
             chapterList.add(chapter)
         }
         }
-        book.latestChapterTitle = chapterList.lastOrNull()?.title
-        book.totalChapterNum = chapterList.size
         return chapterList
         return chapterList
     }
     }
 
 

+ 8 - 2
app/src/main/java/io/legado/app/ui/book/local/rule/TxtTocRuleActivity.kt

@@ -88,7 +88,13 @@ class TxtTocRuleActivity : VMBaseActivity<ActivityTxtTocRuleBinding, TxtTocRuleV
     }
     }
 
 
     override fun del(source: TxtTocRule) {
     override fun del(source: TxtTocRule) {
-        viewModel.del(source)
+        alert(R.string.draw) {
+            setMessage(getString(R.string.sure_del) + "\n" + source.name)
+            noButton()
+            yesButton {
+                viewModel.del(source)
+            }
+        }
     }
     }
 
 
     override fun edit(source: TxtTocRule) {
     override fun edit(source: TxtTocRule) {
@@ -151,7 +157,7 @@ class TxtTocRuleActivity : VMBaseActivity<ActivityTxtTocRuleBinding, TxtTocRuleV
 
 
     private fun delSourceDialog() {
     private fun delSourceDialog() {
         alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
         alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
-            okButton { viewModel.del(*adapter.selection.toTypedArray()) }
+            yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
             noButton()
             noButton()
         }
         }
     }
     }

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -976,7 +976,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     Coroutine.async {
                     Coroutine.async {
                         val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
                         val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
                         val payAction = source.getContentRule().payAction
                         val payAction = source.getContentRule().payAction
-                        if (payAction.isNullOrEmpty()) {
+                        if (payAction.isNullOrBlank()) {
                             throw NoStackTraceException("no pay action")
                             throw NoStackTraceException("no pay action")
                         }
                         }
                         val analyzeRule = AnalyzeRule(book, source)
                         val analyzeRule = AnalyzeRule(book, source)
@@ -984,9 +984,9 @@ class ReadBookActivity : BaseReadBookActivity(),
                         analyzeRule.chapter = chapter
                         analyzeRule.chapter = chapter
                         analyzeRule.evalJS(payAction).toString()
                         analyzeRule.evalJS(payAction).toString()
                     }.onSuccess {
                     }.onSuccess {
-                        if (it.isNotBlank()) {
+                        if (it.isAbsUrl()) {
                             startActivity<WebViewActivity> {
                             startActivity<WebViewActivity> {
-                                putExtra("title", R.string.chapter_pay)
+                                putExtra("title", getString(R.string.chapter_pay))
                                 putExtra("url", it)
                                 putExtra("url", it)
                                 IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
                                 IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
                             }
                             }

+ 3 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -191,6 +191,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
             } else {
             } else {
                 throw NoStackTraceException("进度同步未启用")
                 throw NoStackTraceException("进度同步未启用")
             }
             }
+        }.onError {
+            AppLog.put("拉取阅读进度失败", it)
         }.onSuccess { progress ->
         }.onSuccess { progress ->
             if (progress.durChapterIndex < book.durChapterIndex ||
             if (progress.durChapterIndex < book.durChapterIndex ||
                 (progress.durChapterIndex == book.durChapterIndex
                 (progress.durChapterIndex == book.durChapterIndex
@@ -199,6 +201,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
                 alertSync?.invoke(progress)
                 alertSync?.invoke(progress)
             } else {
             } else {
                 ReadBook.setProgress(progress)
                 ReadBook.setProgress(progress)
+                AppLog.put("自动同步阅读进度成功")
             }
             }
         }
         }
 
 

+ 36 - 1
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -7,6 +7,7 @@ import android.graphics.RectF
 import android.util.AttributeSet
 import android.util.AttributeSet
 import android.view.View
 import android.view.View
 import io.legado.app.R
 import io.legado.app.R
+import io.legado.app.constant.AppLog
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
@@ -44,6 +45,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     val selectEnd = TextPos(0, 0, 0)
     val selectEnd = TextPos(0, 0, 0)
     var textPage: TextPage = TextPage()
     var textPage: TextPage = TextPage()
         private set
         private set
+    private var drawVisibleImageOnly = false
+    private var cacheIncreased = false
+    private val increaseSize = 8 * 1024 * 1024
+    private val maxCacheSize = 256 * 1024 * 1024
 
 
     //滚动参数
     //滚动参数
     private val pageFactory: TextPageFactory get() = callBack.pageFactory
     private val pageFactory: TextPageFactory get() = callBack.pageFactory
@@ -86,6 +91,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         super.onDraw(canvas)
         super.onDraw(canvas)
         canvas.clipRect(visibleRect)
         canvas.clipRect(visibleRect)
         drawPage(canvas)
         drawPage(canvas)
+        drawVisibleImageOnly = false
+        cacheIncreased = false
     }
     }
 
 
     /**
     /**
@@ -173,12 +180,40 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     ) {
     ) {
 
 
         val book = ReadBook.book ?: return
         val book = ReadBook.book ?: return
+        val isVisible = when {
+            lineTop > 0 -> lineTop < height
+            lineTop < 0 -> lineBottom > 0
+            else -> true
+        }
+        if (drawVisibleImageOnly && !isVisible) {
+            return
+        }
+        if (drawVisibleImageOnly &&
+            isVisible &&
+            !cacheIncreased &&
+            ImageProvider.isTriggerRecycled() &&
+            !ImageProvider.isImageAlive(book, textChar.charData)
+        ) {
+            val newSize = ImageProvider.bitmapLruCache.maxSize() + increaseSize
+            if (newSize < maxCacheSize) {
+                ImageProvider.bitmapLruCache.resize(newSize)
+                AppLog.put("图片缓存不够大,自动扩增至${(newSize / 1024 / 1024)}MB。")
+                cacheIncreased = true
+            }
+            return
+        }
         val bitmap = ImageProvider.getImage(
         val bitmap = ImageProvider.getImage(
             book,
             book,
             textChar.charData,
             textChar.charData,
             (textChar.end - textChar.start).toInt(),
             (textChar.end - textChar.start).toInt(),
             (lineBottom - lineTop).toInt()
             (lineBottom - lineTop).toInt()
-        )
+        ) {
+            if (!drawVisibleImageOnly && isVisible) {
+                drawVisibleImageOnly = true
+                invalidate()
+            }
+        } ?: return
+
         val rectF = if (textLine.isImage) {
         val rectF = if (textLine.isImage) {
             RectF(textChar.start, lineTop, textChar.end, lineBottom)
             RectF(textChar.start, lineTop, textChar.end, lineBottom)
         } else {
         } else {

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt

@@ -119,7 +119,7 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
         abortAnim()
         abortAnim()
         if (!hasNext()) return
         if (!hasNext()) return
         setDirection(PageDirection.NEXT)
         setDirection(PageDirection.NEXT)
-        readView.setStartPoint(viewWidth.toFloat(), 0f, false)
+        readView.setStartPoint(viewWidth.toFloat(), 1f, false)
         onAnimStart(animationSpeed)
         onAnimStart(animationSpeed)
     }
     }
 
 
@@ -127,7 +127,7 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
         abortAnim()
         abortAnim()
         if (!hasPrev()) return
         if (!hasPrev()) return
         setDirection(PageDirection.PREV)
         setDirection(PageDirection.PREV)
-        readView.setStartPoint(0f, 0f, false)
+        readView.setStartPoint(0f, viewHeight.toFloat(), false)
         onAnimStart(animationSpeed)
         onAnimStart(animationSpeed)
     }
     }
 
 

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt

@@ -189,7 +189,7 @@ class SimulationPageDelegate(readView: ReadView) : HorizontalPageDelegate(readVi
             dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) {
             dx = if (mCornerX > 0 && mDirection == PageDirection.NEXT) {
                 -(viewWidth + touchX)
                 -(viewWidth + touchX)
             } else {
             } else {
-                (viewWidth - touchX + viewWidth)
+                viewWidth - touchX
             }
             }
             dy = if (mCornerY > 0) {
             dy = if (mCornerY > 0) {
                 (viewHeight - touchY)
                 (viewHeight - touchY)

+ 47 - 4
app/src/main/java/io/legado/app/ui/book/read/page/provider/ImageProvider.kt

@@ -6,15 +6,18 @@ import android.util.Size
 import androidx.collection.LruCache
 import androidx.collection.LruCache
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.AppLog.putDebug
 import io.legado.app.constant.AppLog.putDebug
+import io.legado.app.constant.PageAnim
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.BookHelp
 import io.legado.app.help.BookHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
+import io.legado.app.model.ReadBook
 import io.legado.app.model.localBook.EpubFile
 import io.legado.app.model.localBook.EpubFile
 import io.legado.app.utils.*
 import io.legado.app.utils.*
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import splitties.init.appCtx
 import java.io.File
 import java.io.File
@@ -32,6 +35,7 @@ object ImageProvider {
      */
      */
     private const val M = 1024 * 1024
     private const val M = 1024 * 1024
     val cacheSize get() = AppConfig.bitmapCacheSize * M
     val cacheSize get() = AppConfig.bitmapCacheSize * M
+    var triggerRecycled = false
     val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) {
     val bitmapLruCache = object : LruCache<String, Bitmap>(cacheSize) {
 
 
         override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
         override fun sizeOf(filePath: String, bitmap: Bitmap): Int {
@@ -47,6 +51,7 @@ object ImageProvider {
             //错误图片不能释放,占位用,防止一直重复获取图片
             //错误图片不能释放,占位用,防止一直重复获取图片
             if (oldBitmap != errorBitmap) {
             if (oldBitmap != errorBitmap) {
                 oldBitmap.recycle()
                 oldBitmap.recycle()
+                triggerRecycled = true
                 putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
                 putDebug("ImageProvider: trigger bitmap recycle. URI: $filePath")
                 putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
                 putDebug("ImageProvider : cacheUsage ${size()}bytes / ${maxSize()}bytes")
             }
             }
@@ -110,12 +115,13 @@ object ImageProvider {
         book: Book,
         book: Book,
         src: String,
         src: String,
         width: Int,
         width: Int,
-        height: Int? = null
-    ): Bitmap {
+        height: Int? = null,
+        block: (() -> Unit)? = null
+    ): Bitmap? {
         //src为空白时 可能被净化替换掉了 或者规则失效
         //src为空白时 可能被净化替换掉了 或者规则失效
         if (book.getUseReplaceRule() && src.isBlank()) {
         if (book.getUseReplaceRule() && src.isBlank()) {
-           book.setUseReplaceRule(false)
-           appCtx.toastOnUi(R.string.error_image_url_empty)
+            book.setUseReplaceRule(false)
+            appCtx.toastOnUi(R.string.error_image_url_empty)
         }
         }
         val vFile = BookHelp.getImage(book, src)
         val vFile = BookHelp.getImage(book, src)
         if (!vFile.exists()) return errorBitmap
         if (!vFile.exists()) return errorBitmap
@@ -123,6 +129,30 @@ object ImageProvider {
         //bitmapLruCache的key同一改成缓存文件的路径
         //bitmapLruCache的key同一改成缓存文件的路径
         val cacheBitmap = bitmapLruCache.get(vFile.absolutePath)
         val cacheBitmap = bitmapLruCache.get(vFile.absolutePath)
         if (cacheBitmap != null) return cacheBitmap
         if (cacheBitmap != null) return cacheBitmap
+        if (height != null && ReadBook.pageAnim() == PageAnim.scrollPageAnim) {
+            Coroutine.async {
+                kotlin.runCatching {
+                    val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
+                        ?: throw NoStackTraceException(appCtx.getString(R.string.error_decode_bitmap))
+                    withContext(Main) {
+                        bitmapLruCache.put(vFile.absolutePath, bitmap)
+                    }
+                }.onFailure {
+                    //错误图片占位,防止重复获取
+                    withContext(Main) {
+                        bitmapLruCache.put(vFile.absolutePath, errorBitmap)
+                    }
+                    putDebug(
+                        "ImageProvider: decode bitmap failed. path: ${vFile.absolutePath}\n$it",
+                        it
+                    )
+                }
+                withContext(Main) {
+                    block?.invoke()
+                }
+            }
+            return null
+        }
         @Suppress("BlockingMethodInNonBlockingContext")
         @Suppress("BlockingMethodInNonBlockingContext")
         return kotlin.runCatching {
         return kotlin.runCatching {
             val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
             val bitmap = BitmapUtils.decodeBitmap(vFile.absolutePath, width, height)
@@ -139,4 +169,17 @@ object ImageProvider {
         }.getOrDefault(errorBitmap)
         }.getOrDefault(errorBitmap)
     }
     }
 
 
+    fun isImageAlive(book: Book, src: String): Boolean {
+        val vFile = BookHelp.getImage(book, src)
+        if (!vFile.exists()) return true // 使用 errorBitmap
+        val cacheBitmap = bitmapLruCache.get(vFile.absolutePath)
+        return cacheBitmap != null
+    }
+
+    fun isTriggerRecycled(): Boolean {
+        val tmp = triggerRecycled
+        triggerRecycled = false
+        return tmp
+    }
+
 }
 }

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt

@@ -76,8 +76,8 @@ object RemoteBookWebDav : RemoteBookManager() {
     override suspend fun getRemoteBook(remoteBook: RemoteBook): Uri? {
     override suspend fun getRemoteBook(remoteBook: RemoteBook): Uri? {
         return AppWebDav.authorization?.let {
         return AppWebDav.authorization?.let {
             val webdav = WebDav(remoteBook.path, it)
             val webdav = WebDav(remoteBook.path, it)
-            webdav.download().let { bytes ->
-                LocalBook.saveBookFile(bytes, remoteBook.filename)
+            webdav.downloadInputStream().let { inputStream ->
+                LocalBook.saveBookFile(inputStream, remoteBook.filename)
             }
             }
         }
         }
     }
     }

+ 8 - 3
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -28,7 +28,6 @@ import io.legado.app.lib.theme.primaryColor
 import io.legado.app.lib.theme.primaryTextColor
 import io.legado.app.lib.theme.primaryTextColor
 import io.legado.app.model.CheckSource
 import io.legado.app.model.CheckSource
 import io.legado.app.model.Debug
 import io.legado.app.model.Debug
-import io.legado.app.service.CheckSourceService
 import io.legado.app.ui.association.ImportBookSourceDialog
 import io.legado.app.ui.association.ImportBookSourceDialog
 import io.legado.app.ui.book.local.rule.TxtTocRuleActivity
 import io.legado.app.ui.book.local.rule.TxtTocRuleActivity
 import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
 import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
@@ -336,7 +335,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
 
 
     override fun onClickSelectBarMainAction() {
     override fun onClickSelectBarMainAction() {
         alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
         alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
-            okButton { viewModel.del(*adapter.selection.toTypedArray()) }
+            yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
             noButton()
             noButton()
         }
         }
     }
     }
@@ -589,7 +588,13 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
     }
     }
 
 
     override fun del(bookSource: BookSource) {
     override fun del(bookSource: BookSource) {
-        viewModel.del(bookSource)
+        alert(R.string.draw) {
+            setMessage(getString(R.string.sure_del) + "\n" + bookSource.bookSourceName)
+            noButton()
+            yesButton {
+                viewModel.del(bookSource)
+            }
+        }
     }
     }
 
 
     override fun update(vararg bookSource: BookSource) {
     override fun update(vararg bookSource: BookSource) {

+ 12 - 10
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt

@@ -162,7 +162,10 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
     }
     }
 
 
     override fun onClickSelectBarMainAction() {
     override fun onClickSelectBarMainAction() {
-        delSourceDialog()
+        alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
+            yesButton { viewModel.delSelection(adapter.selection) }
+            noButton()
+        }
     }
     }
 
 
     private fun initSelectActionView() {
     private fun initSelectActionView() {
@@ -172,13 +175,6 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
         binding.selectActionBar.setCallBack(this)
         binding.selectActionBar.setCallBack(this)
     }
     }
 
 
-    private fun delSourceDialog() {
-        alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
-            okButton { viewModel.delSelection(adapter.selection) }
-            noButton()
-        }
-    }
-
     private fun observeReplaceRuleData(searchKey: String? = null) {
     private fun observeReplaceRuleData(searchKey: String? = null) {
         dataInit = false
         dataInit = false
         replaceRuleFlowJob?.cancel()
         replaceRuleFlowJob?.cancel()
@@ -329,8 +325,14 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
     }
     }
 
 
     override fun delete(rule: ReplaceRule) {
     override fun delete(rule: ReplaceRule) {
-        setResult(RESULT_OK)
-        viewModel.delete(rule)
+        alert(R.string.draw) {
+            setMessage(getString(R.string.sure_del) + "\n" + rule.name)
+            noButton()
+            yesButton {
+                setResult(RESULT_OK)
+                viewModel.delete(rule)
+            }
+        }
     }
     }
 
 
     override fun edit(rule: ReplaceRule) {
     override fun edit(rule: ReplaceRule) {

+ 7 - 1
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt

@@ -329,7 +329,13 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
     }
     }
 
 
     override fun del(source: RssSource) {
     override fun del(source: RssSource) {
-        viewModel.del(source)
+        alert(R.string.draw) {
+            setMessage(getString(R.string.sure_del) + "\n" + source.sourceName)
+            noButton()
+            yesButton {
+                viewModel.del(source)
+            }
+        }
     }
     }
 
 
     override fun edit(source: RssSource) {
     override fun edit(source: RssSource) {

+ 0 - 18
app/src/main/java/io/legado/app/utils/BitmapUtils.kt

@@ -10,7 +10,6 @@ import android.graphics.Color
 import com.google.android.renderscript.Toolkit
 import com.google.android.renderscript.Toolkit
 import java.io.FileInputStream
 import java.io.FileInputStream
 import java.io.IOException
 import java.io.IOException
-import java.io.InputStream
 import kotlin.math.*
 import kotlin.math.*
 
 
 
 
@@ -82,23 +81,6 @@ object BitmapUtils {
         }
         }
     }
     }
 
 
-    /** 从path中获取Bitmap图片
-     * @param path 图片路径
-     * @return
-     */
-    @Throws(IOException::class)
-    fun decodeBitmap(inputStream: InputStream): Bitmap? {
-        return inputStream.use {
-            val opts = BitmapFactory.Options()
-            opts.inJustDecodeBounds = true
-
-            BitmapFactory.decodeStream(inputStream, null, opts)
-            opts.inSampleSize = computeSampleSize(opts, -1, 128 * 128)
-            opts.inJustDecodeBounds = false
-            BitmapFactory.decodeStream(inputStream, null, opts)
-        }
-    }
-
     /**
     /**
      * 以最省内存的方式读取本地资源的图片
      * 以最省内存的方式读取本地资源的图片
      * @param context 设备上下文
      * @param context 设备上下文

+ 1 - 1
app/src/main/java/io/legado/app/utils/HtmlFormatter.kt

@@ -14,7 +14,7 @@ object HtmlFormatter {
     private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
     private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
     private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
     private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
     private val formatImagePattern = Pattern.compile(
     private val formatImagePattern = Pattern.compile(
-        "<img[^>]*src *= *\"([^\"{]*\\{(?:[^{}]|\\{[^}]+\\})+\\})\"[^>]*>|<img[^>]*data-[^=]*= *\"([^\"]*)\"[^>]*>|<img[^>]*src *= *\"([^\"]*)\"[^>]*>",
+        "<img[^>]*src *= *\"([^\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})\"[^>]*>|<img[^>]*data-[^=>]*= *\"([^\">]*)\"[^>]*>|<img[^>]*src *= *\"([^\">]*)\"[^>]*>",
         Pattern.CASE_INSENSITIVE
         Pattern.CASE_INSENSITIVE
     )
     )
 
 

+ 24 - 3
app/src/main/java/io/legado/app/web/HttpServer.kt

@@ -7,9 +7,11 @@ import io.legado.app.api.ReturnData
 import io.legado.app.api.controller.BookController
 import io.legado.app.api.controller.BookController
 import io.legado.app.api.controller.BookSourceController
 import io.legado.app.api.controller.BookSourceController
 import io.legado.app.api.controller.RssSourceController
 import io.legado.app.api.controller.RssSourceController
+import io.legado.app.utils.FileUtils
+import io.legado.app.utils.externalFiles
 import io.legado.app.web.utils.AssetsWeb
 import io.legado.app.web.utils.AssetsWeb
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
+import splitties.init.appCtx
+import java.io.*
 
 
 
 
 class HttpServer(port: Int) : NanoHTTPD(port) {
 class HttpServer(port: Int) : NanoHTTPD(port) {
@@ -91,7 +93,26 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
                     byteArray.size.toLong()
                     byteArray.size.toLong()
                 )
                 )
             } else {
             } else {
-                newFixedLengthResponse(Gson().toJson(returnData))
+                try {
+                    newFixedLengthResponse(Gson().toJson(returnData))
+                } catch (e: OutOfMemoryError) {
+                    val path = FileUtils.getPath(
+                        appCtx.externalFiles,
+                        "book_cache",
+                        "bookSources.json"
+                    )
+                    val file = FileUtils.createFileIfNotExist(path)
+                    BufferedWriter(FileWriter(file)).use {
+                        Gson().toJson(returnData, it)
+                    }
+                    val fis = FileInputStream(file)
+                    newFixedLengthResponse(
+                        Response.Status.OK,
+                        "application/json",
+                        fis,
+                        fis.available().toLong()
+                    )
+                }
             }
             }
             response.addHeader("Access-Control-Allow-Methods", "GET, POST")
             response.addHeader("Access-Control-Allow-Methods", "GET, POST")
             response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])
             response.addHeader("Access-Control-Allow-Origin", session.headers["origin"])

+ 1 - 1
gradle.properties

@@ -26,4 +26,4 @@ android.experimental.enableNewResourceShrinker.preciseShrinking=true
 # and none from the library's dependencies, thereby reducing the size of the R class for that library.
 # and none from the library's dependencies, thereby reducing the size of the R class for that library.
 android.nonTransitiveRClass=true
 android.nonTransitiveRClass=true
 #https://chromiumdash.appspot.com/releases?platform=Android
 #https://chromiumdash.appspot.com/releases?platform=Android
-CronetVersion=105.0.5195.68
+CronetVersion=105.0.5195.77

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است